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 get_orderbook_for_fill_simulation(
48 &mut self,
49 instrument: &InstrumentAny,
50 order: &OrderAny,
51 best_bid: Price,
52 best_ask: Price,
53 ) -> Option<OrderBook>;
54}
55
56#[derive(Debug)]
57pub struct ProbabilisticFillState {
58 prob_fill_on_limit: f64,
59 prob_slippage: f64,
60 random_seed: Option<u64>,
61 rng: StdRng,
62}
63
64impl ProbabilisticFillState {
65 pub fn new(
75 prob_fill_on_limit: f64,
76 prob_slippage: f64,
77 random_seed: Option<u64>,
78 ) -> anyhow::Result<Self> {
79 check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")
80 .expect(FAILED);
81 check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage").expect(FAILED);
82 let rng = match random_seed {
83 Some(seed) => StdRng::seed_from_u64(seed),
84 None => StdRng::from_rng(&mut rand::rng()),
85 };
86 Ok(Self {
87 prob_fill_on_limit,
88 prob_slippage,
89 random_seed,
90 rng,
91 })
92 }
93
94 pub fn is_limit_filled(&mut self) -> bool {
95 self.event_success(self.prob_fill_on_limit)
96 }
97
98 pub fn is_slipped(&mut self) -> bool {
99 self.event_success(self.prob_slippage)
100 }
101
102 pub fn random_bool(&mut self, probability: f64) -> bool {
103 self.event_success(probability)
104 }
105
106 fn event_success(&mut self, probability: f64) -> bool {
107 match probability {
108 0.0 => false,
109 1.0 => true,
110 _ => self.rng.random_bool(probability),
111 }
112 }
113}
114
115impl Clone for ProbabilisticFillState {
116 fn clone(&self) -> Self {
117 Self::new(
118 self.prob_fill_on_limit,
119 self.prob_slippage,
120 self.random_seed,
121 )
122 .expect("ProbabilisticFillState clone should not fail with valid parameters")
123 }
124}
125
126const UNLIMITED: u64 = 10_000_000_000;
127
128fn build_l2_book(instrument_id: InstrumentId) -> OrderBook {
129 OrderBook::new(instrument_id, BookType::L2_MBP)
130}
131
132fn add_order(book: &mut OrderBook, side: OrderSide, price: Price, size: Quantity, order_id: u64) {
133 let order = BookOrder::new(side, price, size, order_id);
134 book.add(order, 0, 0, UnixNanos::default());
135}
136
137#[derive(Debug)]
138pub struct DefaultFillModel {
139 state: ProbabilisticFillState,
140}
141
142impl DefaultFillModel {
143 pub fn new(
149 prob_fill_on_limit: f64,
150 prob_slippage: f64,
151 random_seed: Option<u64>,
152 ) -> anyhow::Result<Self> {
153 Ok(Self {
154 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
155 })
156 }
157}
158
159impl Clone for DefaultFillModel {
160 fn clone(&self) -> Self {
161 Self {
162 state: self.state.clone(),
163 }
164 }
165}
166
167impl Default for DefaultFillModel {
168 fn default() -> Self {
169 Self::new(1.0, 0.0, None).unwrap()
170 }
171}
172
173impl Display for DefaultFillModel {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 write!(
176 f,
177 "DefaultFillModel(prob_fill_on_limit: {}, prob_slippage: {})",
178 self.state.prob_fill_on_limit, self.state.prob_slippage
179 )
180 }
181}
182
183impl FillModel for DefaultFillModel {
184 fn is_limit_filled(&mut self) -> bool {
185 self.state.is_limit_filled()
186 }
187
188 fn is_slipped(&mut self) -> bool {
189 self.state.is_slipped()
190 }
191
192 fn get_orderbook_for_fill_simulation(
193 &mut self,
194 _instrument: &InstrumentAny,
195 _order: &OrderAny,
196 _best_bid: Price,
197 _best_ask: Price,
198 ) -> Option<OrderBook> {
199 None
200 }
201}
202
203#[derive(Debug)]
205pub struct BestPriceFillModel {
206 state: ProbabilisticFillState,
207}
208
209impl BestPriceFillModel {
210 pub fn new(
216 prob_fill_on_limit: f64,
217 prob_slippage: f64,
218 random_seed: Option<u64>,
219 ) -> anyhow::Result<Self> {
220 Ok(Self {
221 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
222 })
223 }
224}
225
226impl Clone for BestPriceFillModel {
227 fn clone(&self) -> Self {
228 Self {
229 state: self.state.clone(),
230 }
231 }
232}
233
234impl Default for BestPriceFillModel {
235 fn default() -> Self {
236 Self::new(1.0, 0.0, None).unwrap()
237 }
238}
239
240impl FillModel for BestPriceFillModel {
241 fn is_limit_filled(&mut self) -> bool {
242 self.state.is_limit_filled()
243 }
244
245 fn is_slipped(&mut self) -> bool {
246 self.state.is_slipped()
247 }
248
249 fn get_orderbook_for_fill_simulation(
250 &mut self,
251 instrument: &InstrumentAny,
252 _order: &OrderAny,
253 best_bid: Price,
254 best_ask: Price,
255 ) -> Option<OrderBook> {
256 let mut book = build_l2_book(instrument.id());
257 let size_prec = instrument.size_precision();
258 add_order(
259 &mut book,
260 OrderSide::Buy,
261 best_bid,
262 Quantity::new(UNLIMITED as f64, size_prec),
263 1,
264 );
265 add_order(
266 &mut book,
267 OrderSide::Sell,
268 best_ask,
269 Quantity::new(UNLIMITED as f64, size_prec),
270 2,
271 );
272 Some(book)
273 }
274}
275
276#[derive(Debug)]
278pub struct OneTickSlippageFillModel {
279 state: ProbabilisticFillState,
280}
281
282impl OneTickSlippageFillModel {
283 pub fn new(
289 prob_fill_on_limit: f64,
290 prob_slippage: f64,
291 random_seed: Option<u64>,
292 ) -> anyhow::Result<Self> {
293 Ok(Self {
294 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
295 })
296 }
297}
298
299impl Clone for OneTickSlippageFillModel {
300 fn clone(&self) -> Self {
301 Self {
302 state: self.state.clone(),
303 }
304 }
305}
306
307impl Default for OneTickSlippageFillModel {
308 fn default() -> Self {
309 Self::new(1.0, 0.0, None).unwrap()
310 }
311}
312
313impl FillModel for OneTickSlippageFillModel {
314 fn is_limit_filled(&mut self) -> bool {
315 self.state.is_limit_filled()
316 }
317
318 fn is_slipped(&mut self) -> bool {
319 self.state.is_slipped()
320 }
321
322 fn get_orderbook_for_fill_simulation(
323 &mut self,
324 instrument: &InstrumentAny,
325 _order: &OrderAny,
326 best_bid: Price,
327 best_ask: Price,
328 ) -> Option<OrderBook> {
329 let tick = instrument.price_increment();
330 let size_prec = instrument.size_precision();
331 let mut book = build_l2_book(instrument.id());
332
333 add_order(
334 &mut book,
335 OrderSide::Buy,
336 best_bid - tick,
337 Quantity::new(UNLIMITED as f64, size_prec),
338 1,
339 );
340 add_order(
341 &mut book,
342 OrderSide::Sell,
343 best_ask + tick,
344 Quantity::new(UNLIMITED as f64, size_prec),
345 2,
346 );
347 Some(book)
348 }
349}
350
351#[derive(Debug)]
353pub struct ProbabilisticFillModel {
354 state: ProbabilisticFillState,
355}
356
357impl ProbabilisticFillModel {
358 pub fn new(
364 prob_fill_on_limit: f64,
365 prob_slippage: f64,
366 random_seed: Option<u64>,
367 ) -> anyhow::Result<Self> {
368 Ok(Self {
369 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
370 })
371 }
372}
373
374impl Clone for ProbabilisticFillModel {
375 fn clone(&self) -> Self {
376 Self {
377 state: self.state.clone(),
378 }
379 }
380}
381
382impl Default for ProbabilisticFillModel {
383 fn default() -> Self {
384 Self::new(1.0, 0.0, None).unwrap()
385 }
386}
387
388impl FillModel for ProbabilisticFillModel {
389 fn is_limit_filled(&mut self) -> bool {
390 self.state.is_limit_filled()
391 }
392
393 fn is_slipped(&mut self) -> bool {
394 self.state.is_slipped()
395 }
396
397 fn get_orderbook_for_fill_simulation(
398 &mut self,
399 instrument: &InstrumentAny,
400 _order: &OrderAny,
401 best_bid: Price,
402 best_ask: Price,
403 ) -> Option<OrderBook> {
404 let tick = instrument.price_increment();
405 let size_prec = instrument.size_precision();
406 let mut book = build_l2_book(instrument.id());
407
408 if self.state.random_bool(0.5) {
409 add_order(
410 &mut book,
411 OrderSide::Buy,
412 best_bid,
413 Quantity::new(UNLIMITED as f64, size_prec),
414 1,
415 );
416 add_order(
417 &mut book,
418 OrderSide::Sell,
419 best_ask,
420 Quantity::new(UNLIMITED as f64, size_prec),
421 2,
422 );
423 } else {
424 add_order(
425 &mut book,
426 OrderSide::Buy,
427 best_bid - tick,
428 Quantity::new(UNLIMITED as f64, size_prec),
429 1,
430 );
431 add_order(
432 &mut book,
433 OrderSide::Sell,
434 best_ask + tick,
435 Quantity::new(UNLIMITED as f64, size_prec),
436 2,
437 );
438 }
439 Some(book)
440 }
441}
442
443#[derive(Debug)]
445pub struct TwoTierFillModel {
446 state: ProbabilisticFillState,
447}
448
449impl TwoTierFillModel {
450 pub fn new(
456 prob_fill_on_limit: f64,
457 prob_slippage: f64,
458 random_seed: Option<u64>,
459 ) -> anyhow::Result<Self> {
460 Ok(Self {
461 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
462 })
463 }
464}
465
466impl Clone for TwoTierFillModel {
467 fn clone(&self) -> Self {
468 Self {
469 state: self.state.clone(),
470 }
471 }
472}
473
474impl Default for TwoTierFillModel {
475 fn default() -> Self {
476 Self::new(1.0, 0.0, None).unwrap()
477 }
478}
479
480impl FillModel for TwoTierFillModel {
481 fn is_limit_filled(&mut self) -> bool {
482 self.state.is_limit_filled()
483 }
484
485 fn is_slipped(&mut self) -> bool {
486 self.state.is_slipped()
487 }
488
489 fn get_orderbook_for_fill_simulation(
490 &mut self,
491 instrument: &InstrumentAny,
492 _order: &OrderAny,
493 best_bid: Price,
494 best_ask: Price,
495 ) -> Option<OrderBook> {
496 let tick = instrument.price_increment();
497 let size_prec = instrument.size_precision();
498 let mut book = build_l2_book(instrument.id());
499
500 add_order(
501 &mut book,
502 OrderSide::Buy,
503 best_bid,
504 Quantity::new(10.0, size_prec),
505 1,
506 );
507 add_order(
508 &mut book,
509 OrderSide::Sell,
510 best_ask,
511 Quantity::new(10.0, size_prec),
512 2,
513 );
514 add_order(
515 &mut book,
516 OrderSide::Buy,
517 best_bid - tick,
518 Quantity::new(UNLIMITED as f64, size_prec),
519 3,
520 );
521 add_order(
522 &mut book,
523 OrderSide::Sell,
524 best_ask + tick,
525 Quantity::new(UNLIMITED as f64, size_prec),
526 4,
527 );
528 Some(book)
529 }
530}
531
532#[derive(Debug)]
534pub struct ThreeTierFillModel {
535 state: ProbabilisticFillState,
536}
537
538impl ThreeTierFillModel {
539 pub fn new(
545 prob_fill_on_limit: f64,
546 prob_slippage: f64,
547 random_seed: Option<u64>,
548 ) -> anyhow::Result<Self> {
549 Ok(Self {
550 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
551 })
552 }
553}
554
555impl Clone for ThreeTierFillModel {
556 fn clone(&self) -> Self {
557 Self {
558 state: self.state.clone(),
559 }
560 }
561}
562
563impl Default for ThreeTierFillModel {
564 fn default() -> Self {
565 Self::new(1.0, 0.0, None).unwrap()
566 }
567}
568
569impl FillModel for ThreeTierFillModel {
570 fn is_limit_filled(&mut self) -> bool {
571 self.state.is_limit_filled()
572 }
573
574 fn is_slipped(&mut self) -> bool {
575 self.state.is_slipped()
576 }
577
578 fn get_orderbook_for_fill_simulation(
579 &mut self,
580 instrument: &InstrumentAny,
581 _order: &OrderAny,
582 best_bid: Price,
583 best_ask: Price,
584 ) -> Option<OrderBook> {
585 let tick = instrument.price_increment();
586 let two_ticks = tick + tick;
587 let size_prec = instrument.size_precision();
588 let mut book = build_l2_book(instrument.id());
589
590 add_order(
591 &mut book,
592 OrderSide::Buy,
593 best_bid,
594 Quantity::new(50.0, size_prec),
595 1,
596 );
597 add_order(
598 &mut book,
599 OrderSide::Sell,
600 best_ask,
601 Quantity::new(50.0, size_prec),
602 2,
603 );
604 add_order(
605 &mut book,
606 OrderSide::Buy,
607 best_bid - tick,
608 Quantity::new(30.0, size_prec),
609 3,
610 );
611 add_order(
612 &mut book,
613 OrderSide::Sell,
614 best_ask + tick,
615 Quantity::new(30.0, size_prec),
616 4,
617 );
618 add_order(
619 &mut book,
620 OrderSide::Buy,
621 best_bid - two_ticks,
622 Quantity::new(20.0, size_prec),
623 5,
624 );
625 add_order(
626 &mut book,
627 OrderSide::Sell,
628 best_ask + two_ticks,
629 Quantity::new(20.0, size_prec),
630 6,
631 );
632 Some(book)
633 }
634}
635
636#[derive(Debug)]
638pub struct LimitOrderPartialFillModel {
639 state: ProbabilisticFillState,
640}
641
642impl LimitOrderPartialFillModel {
643 pub fn new(
649 prob_fill_on_limit: f64,
650 prob_slippage: f64,
651 random_seed: Option<u64>,
652 ) -> anyhow::Result<Self> {
653 Ok(Self {
654 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
655 })
656 }
657}
658
659impl Clone for LimitOrderPartialFillModel {
660 fn clone(&self) -> Self {
661 Self {
662 state: self.state.clone(),
663 }
664 }
665}
666
667impl Default for LimitOrderPartialFillModel {
668 fn default() -> Self {
669 Self::new(1.0, 0.0, None).unwrap()
670 }
671}
672
673impl FillModel for LimitOrderPartialFillModel {
674 fn is_limit_filled(&mut self) -> bool {
675 self.state.is_limit_filled()
676 }
677
678 fn is_slipped(&mut self) -> bool {
679 self.state.is_slipped()
680 }
681
682 fn get_orderbook_for_fill_simulation(
683 &mut self,
684 instrument: &InstrumentAny,
685 _order: &OrderAny,
686 best_bid: Price,
687 best_ask: Price,
688 ) -> Option<OrderBook> {
689 let tick = instrument.price_increment();
690 let size_prec = instrument.size_precision();
691 let mut book = build_l2_book(instrument.id());
692
693 add_order(
694 &mut book,
695 OrderSide::Buy,
696 best_bid,
697 Quantity::new(5.0, size_prec),
698 1,
699 );
700 add_order(
701 &mut book,
702 OrderSide::Sell,
703 best_ask,
704 Quantity::new(5.0, size_prec),
705 2,
706 );
707 add_order(
708 &mut book,
709 OrderSide::Buy,
710 best_bid - tick,
711 Quantity::new(UNLIMITED as f64, size_prec),
712 3,
713 );
714 add_order(
715 &mut book,
716 OrderSide::Sell,
717 best_ask + tick,
718 Quantity::new(UNLIMITED as f64, size_prec),
719 4,
720 );
721 Some(book)
722 }
723}
724
725#[derive(Debug)]
728pub struct SizeAwareFillModel {
729 state: ProbabilisticFillState,
730}
731
732impl SizeAwareFillModel {
733 pub fn new(
739 prob_fill_on_limit: f64,
740 prob_slippage: f64,
741 random_seed: Option<u64>,
742 ) -> anyhow::Result<Self> {
743 Ok(Self {
744 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
745 })
746 }
747}
748
749impl Clone for SizeAwareFillModel {
750 fn clone(&self) -> Self {
751 Self {
752 state: self.state.clone(),
753 }
754 }
755}
756
757impl Default for SizeAwareFillModel {
758 fn default() -> Self {
759 Self::new(1.0, 0.0, None).unwrap()
760 }
761}
762
763impl FillModel for SizeAwareFillModel {
764 fn is_limit_filled(&mut self) -> bool {
765 self.state.is_limit_filled()
766 }
767
768 fn is_slipped(&mut self) -> bool {
769 self.state.is_slipped()
770 }
771
772 fn get_orderbook_for_fill_simulation(
773 &mut self,
774 instrument: &InstrumentAny,
775 order: &OrderAny,
776 best_bid: Price,
777 best_ask: Price,
778 ) -> Option<OrderBook> {
779 let tick = instrument.price_increment();
780 let size_prec = instrument.size_precision();
781 let mut book = build_l2_book(instrument.id());
782
783 let threshold = Quantity::new(10.0, size_prec);
784 if order.quantity() <= threshold {
785 add_order(
787 &mut book,
788 OrderSide::Buy,
789 best_bid,
790 Quantity::new(50.0, size_prec),
791 1,
792 );
793 add_order(
794 &mut book,
795 OrderSide::Sell,
796 best_ask,
797 Quantity::new(50.0, size_prec),
798 2,
799 );
800 } else {
801 let remaining = order.quantity() - threshold;
803 add_order(&mut book, OrderSide::Buy, best_bid, threshold, 1);
804 add_order(&mut book, OrderSide::Sell, best_ask, threshold, 2);
805 add_order(&mut book, OrderSide::Buy, best_bid - tick, remaining, 3);
806 add_order(&mut book, OrderSide::Sell, best_ask + tick, remaining, 4);
807 }
808 Some(book)
809 }
810}
811
812#[derive(Debug)]
814pub struct CompetitionAwareFillModel {
815 state: ProbabilisticFillState,
816 liquidity_factor: f64,
817}
818
819impl CompetitionAwareFillModel {
820 pub fn new(
826 prob_fill_on_limit: f64,
827 prob_slippage: f64,
828 random_seed: Option<u64>,
829 liquidity_factor: f64,
830 ) -> anyhow::Result<Self> {
831 Ok(Self {
832 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
833 liquidity_factor,
834 })
835 }
836}
837
838impl Clone for CompetitionAwareFillModel {
839 fn clone(&self) -> Self {
840 Self {
841 state: self.state.clone(),
842 liquidity_factor: self.liquidity_factor,
843 }
844 }
845}
846
847impl Default for CompetitionAwareFillModel {
848 fn default() -> Self {
849 Self::new(1.0, 0.0, None, 0.3).unwrap()
850 }
851}
852
853impl FillModel for CompetitionAwareFillModel {
854 fn is_limit_filled(&mut self) -> bool {
855 self.state.is_limit_filled()
856 }
857
858 fn is_slipped(&mut self) -> bool {
859 self.state.is_slipped()
860 }
861
862 fn get_orderbook_for_fill_simulation(
863 &mut self,
864 instrument: &InstrumentAny,
865 _order: &OrderAny,
866 best_bid: Price,
867 best_ask: Price,
868 ) -> Option<OrderBook> {
869 let size_prec = instrument.size_precision();
870 let mut book = build_l2_book(instrument.id());
871
872 let typical_volume = 1000.0;
873
874 let available_bid = (typical_volume * self.liquidity_factor).max(1.0);
876 let available_ask = (typical_volume * self.liquidity_factor).max(1.0);
877
878 add_order(
879 &mut book,
880 OrderSide::Buy,
881 best_bid,
882 Quantity::new(available_bid, size_prec),
883 1,
884 );
885 add_order(
886 &mut book,
887 OrderSide::Sell,
888 best_ask,
889 Quantity::new(available_ask, size_prec),
890 2,
891 );
892 Some(book)
893 }
894}
895
896#[derive(Debug)]
899pub struct VolumeSensitiveFillModel {
900 state: ProbabilisticFillState,
901 recent_volume: f64,
902}
903
904impl VolumeSensitiveFillModel {
905 pub fn new(
911 prob_fill_on_limit: f64,
912 prob_slippage: f64,
913 random_seed: Option<u64>,
914 ) -> anyhow::Result<Self> {
915 Ok(Self {
916 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
917 recent_volume: 1000.0,
918 })
919 }
920
921 pub fn set_recent_volume(&mut self, volume: f64) {
922 self.recent_volume = volume;
923 }
924}
925
926impl Clone for VolumeSensitiveFillModel {
927 fn clone(&self) -> Self {
928 Self {
929 state: self.state.clone(),
930 recent_volume: self.recent_volume,
931 }
932 }
933}
934
935impl Default for VolumeSensitiveFillModel {
936 fn default() -> Self {
937 Self::new(1.0, 0.0, None).unwrap()
938 }
939}
940
941impl FillModel for VolumeSensitiveFillModel {
942 fn is_limit_filled(&mut self) -> bool {
943 self.state.is_limit_filled()
944 }
945
946 fn is_slipped(&mut self) -> bool {
947 self.state.is_slipped()
948 }
949
950 fn get_orderbook_for_fill_simulation(
951 &mut self,
952 instrument: &InstrumentAny,
953 _order: &OrderAny,
954 best_bid: Price,
955 best_ask: Price,
956 ) -> Option<OrderBook> {
957 let tick = instrument.price_increment();
958 let size_prec = instrument.size_precision();
959 let mut book = build_l2_book(instrument.id());
960
961 let available_volume = (self.recent_volume * 0.25).max(1.0);
963
964 add_order(
965 &mut book,
966 OrderSide::Buy,
967 best_bid,
968 Quantity::new(available_volume, size_prec),
969 1,
970 );
971 add_order(
972 &mut book,
973 OrderSide::Sell,
974 best_ask,
975 Quantity::new(available_volume, size_prec),
976 2,
977 );
978 add_order(
979 &mut book,
980 OrderSide::Buy,
981 best_bid - tick,
982 Quantity::new(UNLIMITED as f64, size_prec),
983 3,
984 );
985 add_order(
986 &mut book,
987 OrderSide::Sell,
988 best_ask + tick,
989 Quantity::new(UNLIMITED as f64, size_prec),
990 4,
991 );
992 Some(book)
993 }
994}
995
996#[derive(Debug)]
999pub struct MarketHoursFillModel {
1000 state: ProbabilisticFillState,
1001 is_low_liquidity: bool,
1002}
1003
1004impl MarketHoursFillModel {
1005 pub fn new(
1011 prob_fill_on_limit: f64,
1012 prob_slippage: f64,
1013 random_seed: Option<u64>,
1014 ) -> anyhow::Result<Self> {
1015 Ok(Self {
1016 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1017 is_low_liquidity: false,
1018 })
1019 }
1020
1021 pub fn set_low_liquidity_period(&mut self, is_low_liquidity: bool) {
1022 self.is_low_liquidity = is_low_liquidity;
1023 }
1024
1025 pub fn is_low_liquidity_period(&self) -> bool {
1026 self.is_low_liquidity
1027 }
1028}
1029
1030impl Clone for MarketHoursFillModel {
1031 fn clone(&self) -> Self {
1032 Self {
1033 state: self.state.clone(),
1034 is_low_liquidity: self.is_low_liquidity,
1035 }
1036 }
1037}
1038
1039impl Default for MarketHoursFillModel {
1040 fn default() -> Self {
1041 Self::new(1.0, 0.0, None).unwrap()
1042 }
1043}
1044
1045impl FillModel for MarketHoursFillModel {
1046 fn is_limit_filled(&mut self) -> bool {
1047 self.state.is_limit_filled()
1048 }
1049
1050 fn is_slipped(&mut self) -> bool {
1051 self.state.is_slipped()
1052 }
1053
1054 fn get_orderbook_for_fill_simulation(
1055 &mut self,
1056 instrument: &InstrumentAny,
1057 _order: &OrderAny,
1058 best_bid: Price,
1059 best_ask: Price,
1060 ) -> Option<OrderBook> {
1061 let tick = instrument.price_increment();
1062 let size_prec = instrument.size_precision();
1063 let mut book = build_l2_book(instrument.id());
1064 let normal_volume = 500.0;
1065
1066 if self.is_low_liquidity {
1067 add_order(
1068 &mut book,
1069 OrderSide::Buy,
1070 best_bid - tick,
1071 Quantity::new(normal_volume, size_prec),
1072 1,
1073 );
1074 add_order(
1075 &mut book,
1076 OrderSide::Sell,
1077 best_ask + tick,
1078 Quantity::new(normal_volume, size_prec),
1079 2,
1080 );
1081 } else {
1082 add_order(
1083 &mut book,
1084 OrderSide::Buy,
1085 best_bid,
1086 Quantity::new(normal_volume, size_prec),
1087 1,
1088 );
1089 add_order(
1090 &mut book,
1091 OrderSide::Sell,
1092 best_ask,
1093 Quantity::new(normal_volume, size_prec),
1094 2,
1095 );
1096 }
1097 Some(book)
1098 }
1099}
1100
1101#[derive(Clone, Debug)]
1102pub enum FillModelAny {
1103 Default(DefaultFillModel),
1104 BestPrice(BestPriceFillModel),
1105 OneTickSlippage(OneTickSlippageFillModel),
1106 Probabilistic(ProbabilisticFillModel),
1107 TwoTier(TwoTierFillModel),
1108 ThreeTier(ThreeTierFillModel),
1109 LimitOrderPartialFill(LimitOrderPartialFillModel),
1110 SizeAware(SizeAwareFillModel),
1111 CompetitionAware(CompetitionAwareFillModel),
1112 VolumeSensitive(VolumeSensitiveFillModel),
1113 MarketHours(MarketHoursFillModel),
1114}
1115
1116impl FillModel for FillModelAny {
1117 fn is_limit_filled(&mut self) -> bool {
1118 match self {
1119 Self::Default(m) => m.is_limit_filled(),
1120 Self::BestPrice(m) => m.is_limit_filled(),
1121 Self::OneTickSlippage(m) => m.is_limit_filled(),
1122 Self::Probabilistic(m) => m.is_limit_filled(),
1123 Self::TwoTier(m) => m.is_limit_filled(),
1124 Self::ThreeTier(m) => m.is_limit_filled(),
1125 Self::LimitOrderPartialFill(m) => m.is_limit_filled(),
1126 Self::SizeAware(m) => m.is_limit_filled(),
1127 Self::CompetitionAware(m) => m.is_limit_filled(),
1128 Self::VolumeSensitive(m) => m.is_limit_filled(),
1129 Self::MarketHours(m) => m.is_limit_filled(),
1130 }
1131 }
1132
1133 fn is_slipped(&mut self) -> bool {
1134 match self {
1135 Self::Default(m) => m.is_slipped(),
1136 Self::BestPrice(m) => m.is_slipped(),
1137 Self::OneTickSlippage(m) => m.is_slipped(),
1138 Self::Probabilistic(m) => m.is_slipped(),
1139 Self::TwoTier(m) => m.is_slipped(),
1140 Self::ThreeTier(m) => m.is_slipped(),
1141 Self::LimitOrderPartialFill(m) => m.is_slipped(),
1142 Self::SizeAware(m) => m.is_slipped(),
1143 Self::CompetitionAware(m) => m.is_slipped(),
1144 Self::VolumeSensitive(m) => m.is_slipped(),
1145 Self::MarketHours(m) => m.is_slipped(),
1146 }
1147 }
1148
1149 fn get_orderbook_for_fill_simulation(
1150 &mut self,
1151 instrument: &InstrumentAny,
1152 order: &OrderAny,
1153 best_bid: Price,
1154 best_ask: Price,
1155 ) -> Option<OrderBook> {
1156 match self {
1157 Self::Default(m) => {
1158 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1159 }
1160 Self::BestPrice(m) => {
1161 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1162 }
1163 Self::OneTickSlippage(m) => {
1164 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1165 }
1166 Self::Probabilistic(m) => {
1167 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1168 }
1169 Self::TwoTier(m) => {
1170 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1171 }
1172 Self::ThreeTier(m) => {
1173 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1174 }
1175 Self::LimitOrderPartialFill(m) => {
1176 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1177 }
1178 Self::SizeAware(m) => {
1179 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1180 }
1181 Self::CompetitionAware(m) => {
1182 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1183 }
1184 Self::VolumeSensitive(m) => {
1185 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1186 }
1187 Self::MarketHours(m) => {
1188 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1189 }
1190 }
1191 }
1192}
1193
1194impl Default for FillModelAny {
1195 fn default() -> Self {
1196 Self::Default(DefaultFillModel::default())
1197 }
1198}
1199
1200impl Display for FillModelAny {
1201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1202 match self {
1203 Self::Default(m) => write!(f, "{m}"),
1204 Self::BestPrice(_) => write!(f, "BestPriceFillModel"),
1205 Self::OneTickSlippage(_) => write!(f, "OneTickSlippageFillModel"),
1206 Self::Probabilistic(_) => write!(f, "ProbabilisticFillModel"),
1207 Self::TwoTier(_) => write!(f, "TwoTierFillModel"),
1208 Self::ThreeTier(_) => write!(f, "ThreeTierFillModel"),
1209 Self::LimitOrderPartialFill(_) => write!(f, "LimitOrderPartialFillModel"),
1210 Self::SizeAware(_) => write!(f, "SizeAwareFillModel"),
1211 Self::CompetitionAware(_) => write!(f, "CompetitionAwareFillModel"),
1212 Self::VolumeSensitive(_) => write!(f, "VolumeSensitiveFillModel"),
1213 Self::MarketHours(_) => write!(f, "MarketHoursFillModel"),
1214 }
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use nautilus_model::{
1221 enums::OrderType, instruments::stubs::audusd_sim, orders::builder::OrderTestBuilder,
1222 };
1223 use rstest::{fixture, rstest};
1224
1225 use super::*;
1226
1227 #[fixture]
1228 fn fill_model() -> DefaultFillModel {
1229 let seed = 42;
1230 DefaultFillModel::new(0.5, 0.1, Some(seed)).unwrap()
1231 }
1232
1233 #[rstest]
1234 #[should_panic(
1235 expected = "Condition failed: invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
1236 )]
1237 fn test_fill_model_param_prob_fill_on_limit_error() {
1238 let _ = DefaultFillModel::new(1.1, 0.1, None).unwrap();
1239 }
1240
1241 #[rstest]
1242 #[should_panic(
1243 expected = "Condition failed: invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
1244 )]
1245 fn test_fill_model_param_prob_slippage_error() {
1246 let _ = DefaultFillModel::new(0.5, 1.1, None).unwrap();
1247 }
1248
1249 #[rstest]
1250 fn test_fill_model_is_limit_filled(mut fill_model: DefaultFillModel) {
1251 let result = fill_model.is_limit_filled();
1253 assert!(!result);
1254 }
1255
1256 #[rstest]
1257 fn test_fill_model_is_slipped(mut fill_model: DefaultFillModel) {
1258 let result = fill_model.is_slipped();
1260 assert!(!result);
1261 }
1262
1263 #[rstest]
1264 fn test_default_fill_model_returns_none() {
1265 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1266 let order = OrderTestBuilder::new(OrderType::Market)
1267 .instrument_id(instrument.id())
1268 .side(OrderSide::Buy)
1269 .quantity(Quantity::from(100_000))
1270 .build();
1271
1272 let mut model = DefaultFillModel::default();
1273 let result = model.get_orderbook_for_fill_simulation(
1274 &instrument,
1275 &order,
1276 Price::from("0.80000"),
1277 Price::from("0.80010"),
1278 );
1279 assert!(result.is_none());
1280 }
1281
1282 #[rstest]
1283 fn test_best_price_fill_model_returns_book() {
1284 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1285 let order = OrderTestBuilder::new(OrderType::Market)
1286 .instrument_id(instrument.id())
1287 .side(OrderSide::Buy)
1288 .quantity(Quantity::from(100_000))
1289 .build();
1290
1291 let mut model = BestPriceFillModel::default();
1292 let result = model.get_orderbook_for_fill_simulation(
1293 &instrument,
1294 &order,
1295 Price::from("0.80000"),
1296 Price::from("0.80010"),
1297 );
1298 assert!(result.is_some());
1299 let book = result.unwrap();
1300 assert_eq!(book.best_bid_price().unwrap(), Price::from("0.80000"));
1301 assert_eq!(book.best_ask_price().unwrap(), Price::from("0.80010"));
1302 }
1303
1304 #[rstest]
1305 fn test_one_tick_slippage_fill_model() {
1306 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1307 let order = OrderTestBuilder::new(OrderType::Market)
1308 .instrument_id(instrument.id())
1309 .side(OrderSide::Buy)
1310 .quantity(Quantity::from(100_000))
1311 .build();
1312
1313 let tick = instrument.price_increment();
1314 let best_bid = Price::from("0.80000");
1315 let best_ask = Price::from("0.80010");
1316
1317 let mut model = OneTickSlippageFillModel::default();
1318 let result =
1319 model.get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask);
1320 assert!(result.is_some());
1321 let book = result.unwrap();
1322
1323 assert_eq!(book.best_bid_price().unwrap(), best_bid - tick);
1324 assert_eq!(book.best_ask_price().unwrap(), best_ask + tick);
1325 }
1326
1327 #[rstest]
1328 fn test_fill_model_any_dispatch() {
1329 let model = FillModelAny::default();
1330 assert!(matches!(model, FillModelAny::Default(_)));
1331 }
1332
1333 #[rstest]
1334 fn test_fill_model_any_is_limit_filled() {
1335 let mut model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.1, Some(42)).unwrap());
1336 let result = model.is_limit_filled();
1337 assert!(!result);
1338 }
1339}