1use std::{
17 cell::RefCell,
18 fmt::{Debug, Display},
19 rc::Rc,
20};
21
22#[cfg(all(feature = "simulation", madsim))]
23use madsim::rand::RngCore;
24use nautilus_core::{
25 UnixNanos,
26 correctness::{check_in_range_inclusive_f64, check_non_negative_f64},
27};
28use nautilus_model::{
29 data::order::BookOrder,
30 enums::{BookType, OrderSide},
31 identifiers::InstrumentId,
32 instruments::{Instrument, InstrumentAny},
33 orderbook::OrderBook,
34 orders::{Order, OrderAny},
35 types::{Price, Quantity},
36};
37use rand::{RngExt, SeedableRng, rngs::StdRng};
38use rust_decimal::Decimal;
39use rust_decimal_macros::dec;
40
41const UNLIMITED_LIQUIDITY_UNITS: u64 = 10_000_000_000;
43
44fn unlimited_liquidity(precision: u8) -> Quantity {
45 Quantity::from_mantissa_exponent(UNLIMITED_LIQUIDITY_UNITS, 0, precision)
46}
47
48pub trait FillModel {
49 fn is_limit_filled(&mut self) -> anyhow::Result<bool>;
55
56 fn is_slipped(&mut self) -> anyhow::Result<bool>;
62
63 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
73 Ok(false)
74 }
75
76 fn get_orderbook_for_fill_simulation(
90 &mut self,
91 instrument: &InstrumentAny,
92 order: &OrderAny,
93 best_bid: Option<Price>,
94 best_ask: Option<Price>,
95 ) -> anyhow::Result<Option<OrderBook>>;
96}
97
98#[derive(Clone)]
100pub struct FillModelHandle(Rc<RefCell<dyn FillModel>>);
101
102impl FillModelHandle {
103 #[must_use]
105 pub fn new<T>(model: T) -> Self
106 where
107 T: FillModel + 'static,
108 {
109 Self(Rc::new(RefCell::new(model)))
110 }
111
112 #[must_use]
114 pub fn from_rc(model: Rc<RefCell<dyn FillModel>>) -> Self {
115 Self(model)
116 }
117}
118
119impl Debug for FillModelHandle {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.debug_tuple(stringify!(FillModelHandle))
122 .field(&"<dyn FillModel>")
123 .finish()
124 }
125}
126
127impl FillModel for FillModelHandle {
128 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
129 self.0.borrow_mut().is_limit_filled()
130 }
131
132 fn is_slipped(&mut self) -> anyhow::Result<bool> {
133 self.0.borrow_mut().is_slipped()
134 }
135
136 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
137 self.0.borrow().fill_limit_inside_spread()
138 }
139
140 fn get_orderbook_for_fill_simulation(
141 &mut self,
142 instrument: &InstrumentAny,
143 order: &OrderAny,
144 best_bid: Option<Price>,
145 best_ask: Option<Price>,
146 ) -> anyhow::Result<Option<OrderBook>> {
147 self.0
148 .borrow_mut()
149 .get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
150 }
151}
152
153impl Default for FillModelHandle {
154 fn default() -> Self {
155 FillModelAny::default().into()
156 }
157}
158
159impl From<FillModelAny> for FillModelHandle {
160 fn from(model: FillModelAny) -> Self {
161 Self::new(model)
162 }
163}
164
165#[derive(Debug)]
166pub struct ProbabilisticFillState {
167 prob_fill_on_limit: f64,
168 prob_slippage: f64,
169 random_seed: Option<u64>,
170 rng: StdRng,
171}
172
173impl ProbabilisticFillState {
174 pub fn new(
180 prob_fill_on_limit: f64,
181 prob_slippage: f64,
182 random_seed: Option<u64>,
183 ) -> anyhow::Result<Self> {
184 check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")?;
185 check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage")?;
186 let rng = match random_seed {
187 Some(seed) => StdRng::seed_from_u64(seed),
188 None => default_std_rng(),
189 };
190 Ok(Self {
191 prob_fill_on_limit,
192 prob_slippage,
193 random_seed,
194 rng,
195 })
196 }
197
198 pub fn is_limit_filled(&mut self) -> bool {
199 self.event_success(self.prob_fill_on_limit)
200 }
201
202 pub fn is_slipped(&mut self) -> bool {
203 self.event_success(self.prob_slippage)
204 }
205
206 pub fn random_bool(&mut self, probability: f64) -> bool {
207 self.event_success(probability)
208 }
209
210 fn event_success(&mut self, probability: f64) -> bool {
212 if probability <= 0.0 {
213 false
214 } else if probability >= 1.0 {
215 true
216 } else {
217 self.rng.random_bool(probability)
218 }
219 }
220}
221
222impl Clone for ProbabilisticFillState {
223 fn clone(&self) -> Self {
224 Self::new(
225 self.prob_fill_on_limit,
226 self.prob_slippage,
227 self.random_seed,
228 )
229 .expect("ProbabilisticFillState clone should not fail with valid parameters")
230 }
231}
232
233fn default_std_rng() -> StdRng {
234 #[cfg(all(feature = "simulation", madsim))]
235 {
236 if madsim::runtime::Handle::try_current().is_ok() {
241 let mut seed = [0u8; 32];
242 madsim::rand::thread_rng().fill_bytes(&mut seed);
243 return StdRng::from_seed(seed);
244 }
245 }
246
247 StdRng::from_rng(&mut rand::rng()) }
249
250fn build_l2_book(instrument_id: InstrumentId) -> OrderBook {
251 OrderBook::new(instrument_id, BookType::L2_MBP)
252}
253
254fn add_order(
256 book: &mut OrderBook,
257 instrument: &InstrumentAny,
258 side: OrderSide,
259 price: Price,
260 size: Quantity,
261 order_id: u64,
262) {
263 if !is_price_tradable(instrument, price) {
264 return;
265 }
266
267 let order = BookOrder::new(side, price, size, order_id);
268 book.add(order, 0, 0, UnixNanos::default());
269}
270
271pub(crate) fn is_price_tradable(instrument: &InstrumentAny, price: Price) -> bool {
272 (instrument.allows_negative_price() || price.is_positive())
273 && instrument.min_price().is_none_or(|min| price >= min)
274 && instrument.max_price().is_none_or(|max| price <= max)
275}
276
277#[derive(Debug)]
278#[cfg_attr(
279 feature = "python",
280 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
281)]
282#[cfg_attr(
283 feature = "python",
284 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
285)]
286pub struct DefaultFillModel {
287 state: ProbabilisticFillState,
288}
289
290impl DefaultFillModel {
291 pub fn new(
297 prob_fill_on_limit: f64,
298 prob_slippage: f64,
299 random_seed: Option<u64>,
300 ) -> anyhow::Result<Self> {
301 Ok(Self {
302 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
303 })
304 }
305}
306
307impl Clone for DefaultFillModel {
308 fn clone(&self) -> Self {
309 Self {
310 state: self.state.clone(),
311 }
312 }
313}
314
315impl Default for DefaultFillModel {
316 fn default() -> Self {
317 Self::new(1.0, 0.0, None).unwrap()
318 }
319}
320
321impl Display for DefaultFillModel {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(
324 f,
325 "DefaultFillModel(prob_fill_on_limit={}, prob_slippage={})",
326 self.state.prob_fill_on_limit, self.state.prob_slippage
327 )
328 }
329}
330
331impl FillModel for DefaultFillModel {
332 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
333 Ok(self.state.is_limit_filled())
334 }
335
336 fn is_slipped(&mut self) -> anyhow::Result<bool> {
337 Ok(self.state.is_slipped())
338 }
339
340 fn get_orderbook_for_fill_simulation(
341 &mut self,
342 _instrument: &InstrumentAny,
343 _order: &OrderAny,
344 _best_bid: Option<Price>,
345 _best_ask: Option<Price>,
346 ) -> anyhow::Result<Option<OrderBook>> {
347 Ok(None)
348 }
349}
350
351#[derive(Debug)]
353#[cfg_attr(
354 feature = "python",
355 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
356)]
357#[cfg_attr(
358 feature = "python",
359 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
360)]
361pub struct BestPriceFillModel {
362 state: ProbabilisticFillState,
363}
364
365impl BestPriceFillModel {
366 pub fn new(
372 prob_fill_on_limit: f64,
373 prob_slippage: f64,
374 random_seed: Option<u64>,
375 ) -> anyhow::Result<Self> {
376 Ok(Self {
377 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
378 })
379 }
380}
381
382impl Clone for BestPriceFillModel {
383 fn clone(&self) -> Self {
384 Self {
385 state: self.state.clone(),
386 }
387 }
388}
389
390impl Default for BestPriceFillModel {
391 fn default() -> Self {
392 Self::new(1.0, 0.0, None).unwrap()
393 }
394}
395
396impl FillModel for BestPriceFillModel {
397 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
398 Ok(self.state.is_limit_filled())
399 }
400
401 fn is_slipped(&mut self) -> anyhow::Result<bool> {
402 Ok(self.state.is_slipped())
403 }
404
405 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
406 Ok(true)
407 }
408
409 fn get_orderbook_for_fill_simulation(
410 &mut self,
411 instrument: &InstrumentAny,
412 order: &OrderAny,
413 best_bid: Option<Price>,
414 best_ask: Option<Price>,
415 ) -> anyhow::Result<Option<OrderBook>> {
416 let (Some(mut best_bid), Some(mut best_ask)) = (best_bid, best_ask) else {
417 return Ok(None);
418 };
419
420 if let Some(price) = order.price() {
423 match order.order_side() {
424 OrderSide::Buy if price < best_ask => {
425 if price < best_bid {
426 return Ok(None);
427 }
428
429 best_ask = price;
430 }
431 OrderSide::Sell if price > best_bid => {
432 if price > best_ask {
433 return Ok(None);
434 }
435
436 best_bid = price;
437 }
438 _ => {}
439 }
440 }
441
442 let mut book = build_l2_book(instrument.id());
443 let size_prec = instrument.size_precision();
444 add_order(
445 &mut book,
446 instrument,
447 OrderSide::Buy,
448 best_bid,
449 unlimited_liquidity(size_prec),
450 1,
451 );
452 add_order(
453 &mut book,
454 instrument,
455 OrderSide::Sell,
456 best_ask,
457 unlimited_liquidity(size_prec),
458 2,
459 );
460 Ok(Some(book))
461 }
462}
463
464#[derive(Debug)]
466#[cfg_attr(
467 feature = "python",
468 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
469)]
470#[cfg_attr(
471 feature = "python",
472 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
473)]
474pub struct OneTickSlippageFillModel {
475 state: ProbabilisticFillState,
476}
477
478impl OneTickSlippageFillModel {
479 pub fn new(
485 prob_fill_on_limit: f64,
486 prob_slippage: f64,
487 random_seed: Option<u64>,
488 ) -> anyhow::Result<Self> {
489 Ok(Self {
490 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
491 })
492 }
493}
494
495impl Clone for OneTickSlippageFillModel {
496 fn clone(&self) -> Self {
497 Self {
498 state: self.state.clone(),
499 }
500 }
501}
502
503impl Default for OneTickSlippageFillModel {
504 fn default() -> Self {
505 Self::new(1.0, 0.0, None).unwrap()
506 }
507}
508
509impl FillModel for OneTickSlippageFillModel {
510 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
511 Ok(self.state.is_limit_filled())
512 }
513
514 fn is_slipped(&mut self) -> anyhow::Result<bool> {
515 Ok(self.state.is_slipped())
516 }
517
518 fn get_orderbook_for_fill_simulation(
519 &mut self,
520 instrument: &InstrumentAny,
521 _order: &OrderAny,
522 best_bid: Option<Price>,
523 best_ask: Option<Price>,
524 ) -> anyhow::Result<Option<OrderBook>> {
525 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
526 return Ok(None);
527 };
528
529 let tick = instrument.price_increment();
530 let size_prec = instrument.size_precision();
531 let mut book = build_l2_book(instrument.id());
532
533 add_order(
534 &mut book,
535 instrument,
536 OrderSide::Buy,
537 best_bid - tick,
538 unlimited_liquidity(size_prec),
539 1,
540 );
541 add_order(
542 &mut book,
543 instrument,
544 OrderSide::Sell,
545 best_ask + tick,
546 unlimited_liquidity(size_prec),
547 2,
548 );
549 Ok(Some(book))
550 }
551}
552
553#[derive(Debug)]
555#[cfg_attr(
556 feature = "python",
557 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
558)]
559#[cfg_attr(
560 feature = "python",
561 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
562)]
563pub struct ProbabilisticFillModel {
564 state: ProbabilisticFillState,
565}
566
567impl ProbabilisticFillModel {
568 pub fn new(
574 prob_fill_on_limit: f64,
575 prob_slippage: f64,
576 random_seed: Option<u64>,
577 ) -> anyhow::Result<Self> {
578 Ok(Self {
579 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
580 })
581 }
582}
583
584impl Clone for ProbabilisticFillModel {
585 fn clone(&self) -> Self {
586 Self {
587 state: self.state.clone(),
588 }
589 }
590}
591
592impl Default for ProbabilisticFillModel {
593 fn default() -> Self {
594 Self::new(1.0, 0.0, None).unwrap()
595 }
596}
597
598impl FillModel for ProbabilisticFillModel {
599 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
600 Ok(self.state.is_limit_filled())
601 }
602
603 fn is_slipped(&mut self) -> anyhow::Result<bool> {
604 Ok(self.state.is_slipped())
605 }
606
607 fn get_orderbook_for_fill_simulation(
608 &mut self,
609 instrument: &InstrumentAny,
610 _order: &OrderAny,
611 best_bid: Option<Price>,
612 best_ask: Option<Price>,
613 ) -> anyhow::Result<Option<OrderBook>> {
614 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
615 return Ok(None);
616 };
617
618 let tick = instrument.price_increment();
619 let size_prec = instrument.size_precision();
620 let mut book = build_l2_book(instrument.id());
621
622 if self.state.random_bool(0.5) {
623 add_order(
624 &mut book,
625 instrument,
626 OrderSide::Buy,
627 best_bid,
628 unlimited_liquidity(size_prec),
629 1,
630 );
631 add_order(
632 &mut book,
633 instrument,
634 OrderSide::Sell,
635 best_ask,
636 unlimited_liquidity(size_prec),
637 2,
638 );
639 } else {
640 add_order(
641 &mut book,
642 instrument,
643 OrderSide::Buy,
644 best_bid - tick,
645 unlimited_liquidity(size_prec),
646 1,
647 );
648 add_order(
649 &mut book,
650 instrument,
651 OrderSide::Sell,
652 best_ask + tick,
653 unlimited_liquidity(size_prec),
654 2,
655 );
656 }
657 Ok(Some(book))
658 }
659}
660
661#[derive(Debug)]
663#[cfg_attr(
664 feature = "python",
665 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
666)]
667#[cfg_attr(
668 feature = "python",
669 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
670)]
671pub struct TwoTierFillModel {
672 state: ProbabilisticFillState,
673}
674
675impl TwoTierFillModel {
676 pub fn new(
682 prob_fill_on_limit: f64,
683 prob_slippage: f64,
684 random_seed: Option<u64>,
685 ) -> anyhow::Result<Self> {
686 Ok(Self {
687 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
688 })
689 }
690}
691
692impl Clone for TwoTierFillModel {
693 fn clone(&self) -> Self {
694 Self {
695 state: self.state.clone(),
696 }
697 }
698}
699
700impl Default for TwoTierFillModel {
701 fn default() -> Self {
702 Self::new(1.0, 0.0, None).unwrap()
703 }
704}
705
706impl FillModel for TwoTierFillModel {
707 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
708 Ok(self.state.is_limit_filled())
709 }
710
711 fn is_slipped(&mut self) -> anyhow::Result<bool> {
712 Ok(self.state.is_slipped())
713 }
714
715 fn get_orderbook_for_fill_simulation(
716 &mut self,
717 instrument: &InstrumentAny,
718 _order: &OrderAny,
719 best_bid: Option<Price>,
720 best_ask: Option<Price>,
721 ) -> anyhow::Result<Option<OrderBook>> {
722 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
723 return Ok(None);
724 };
725
726 let tick = instrument.price_increment();
727 let size_prec = instrument.size_precision();
728 let mut book = build_l2_book(instrument.id());
729
730 add_order(
731 &mut book,
732 instrument,
733 OrderSide::Buy,
734 best_bid,
735 Quantity::new(10.0, size_prec),
736 1,
737 );
738 add_order(
739 &mut book,
740 instrument,
741 OrderSide::Sell,
742 best_ask,
743 Quantity::new(10.0, size_prec),
744 2,
745 );
746 add_order(
747 &mut book,
748 instrument,
749 OrderSide::Buy,
750 best_bid - tick,
751 unlimited_liquidity(size_prec),
752 3,
753 );
754 add_order(
755 &mut book,
756 instrument,
757 OrderSide::Sell,
758 best_ask + tick,
759 unlimited_liquidity(size_prec),
760 4,
761 );
762 Ok(Some(book))
763 }
764}
765
766#[derive(Debug)]
768#[cfg_attr(
769 feature = "python",
770 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
771)]
772#[cfg_attr(
773 feature = "python",
774 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
775)]
776pub struct ThreeTierFillModel {
777 state: ProbabilisticFillState,
778}
779
780impl ThreeTierFillModel {
781 pub fn new(
787 prob_fill_on_limit: f64,
788 prob_slippage: f64,
789 random_seed: Option<u64>,
790 ) -> anyhow::Result<Self> {
791 Ok(Self {
792 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
793 })
794 }
795}
796
797impl Clone for ThreeTierFillModel {
798 fn clone(&self) -> Self {
799 Self {
800 state: self.state.clone(),
801 }
802 }
803}
804
805impl Default for ThreeTierFillModel {
806 fn default() -> Self {
807 Self::new(1.0, 0.0, None).unwrap()
808 }
809}
810
811impl FillModel for ThreeTierFillModel {
812 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
813 Ok(self.state.is_limit_filled())
814 }
815
816 fn is_slipped(&mut self) -> anyhow::Result<bool> {
817 Ok(self.state.is_slipped())
818 }
819
820 fn get_orderbook_for_fill_simulation(
821 &mut self,
822 instrument: &InstrumentAny,
823 _order: &OrderAny,
824 best_bid: Option<Price>,
825 best_ask: Option<Price>,
826 ) -> anyhow::Result<Option<OrderBook>> {
827 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
828 return Ok(None);
829 };
830
831 let tick = instrument.price_increment();
832 let two_ticks = tick + tick;
833 let size_prec = instrument.size_precision();
834 let mut book = build_l2_book(instrument.id());
835
836 add_order(
837 &mut book,
838 instrument,
839 OrderSide::Buy,
840 best_bid,
841 Quantity::new(50.0, size_prec),
842 1,
843 );
844 add_order(
845 &mut book,
846 instrument,
847 OrderSide::Sell,
848 best_ask,
849 Quantity::new(50.0, size_prec),
850 2,
851 );
852 add_order(
853 &mut book,
854 instrument,
855 OrderSide::Buy,
856 best_bid - tick,
857 Quantity::new(30.0, size_prec),
858 3,
859 );
860 add_order(
861 &mut book,
862 instrument,
863 OrderSide::Sell,
864 best_ask + tick,
865 Quantity::new(30.0, size_prec),
866 4,
867 );
868 add_order(
869 &mut book,
870 instrument,
871 OrderSide::Buy,
872 best_bid - two_ticks,
873 Quantity::new(20.0, size_prec),
874 5,
875 );
876 add_order(
877 &mut book,
878 instrument,
879 OrderSide::Sell,
880 best_ask + two_ticks,
881 Quantity::new(20.0, size_prec),
882 6,
883 );
884 Ok(Some(book))
885 }
886}
887
888#[derive(Debug)]
890#[cfg_attr(
891 feature = "python",
892 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
893)]
894#[cfg_attr(
895 feature = "python",
896 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
897)]
898pub struct LimitOrderPartialFillModel {
899 state: ProbabilisticFillState,
900}
901
902impl LimitOrderPartialFillModel {
903 pub fn new(
909 prob_fill_on_limit: f64,
910 prob_slippage: f64,
911 random_seed: Option<u64>,
912 ) -> anyhow::Result<Self> {
913 Ok(Self {
914 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
915 })
916 }
917}
918
919impl Clone for LimitOrderPartialFillModel {
920 fn clone(&self) -> Self {
921 Self {
922 state: self.state.clone(),
923 }
924 }
925}
926
927impl Default for LimitOrderPartialFillModel {
928 fn default() -> Self {
929 Self::new(1.0, 0.0, None).unwrap()
930 }
931}
932
933impl FillModel for LimitOrderPartialFillModel {
934 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
935 Ok(self.state.is_limit_filled())
936 }
937
938 fn is_slipped(&mut self) -> anyhow::Result<bool> {
939 Ok(self.state.is_slipped())
940 }
941
942 fn get_orderbook_for_fill_simulation(
943 &mut self,
944 instrument: &InstrumentAny,
945 _order: &OrderAny,
946 best_bid: Option<Price>,
947 best_ask: Option<Price>,
948 ) -> anyhow::Result<Option<OrderBook>> {
949 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
950 return Ok(None);
951 };
952
953 let tick = instrument.price_increment();
954 let size_prec = instrument.size_precision();
955 let mut book = build_l2_book(instrument.id());
956
957 add_order(
958 &mut book,
959 instrument,
960 OrderSide::Buy,
961 best_bid,
962 Quantity::new(5.0, size_prec),
963 1,
964 );
965 add_order(
966 &mut book,
967 instrument,
968 OrderSide::Sell,
969 best_ask,
970 Quantity::new(5.0, size_prec),
971 2,
972 );
973 add_order(
974 &mut book,
975 instrument,
976 OrderSide::Buy,
977 best_bid - tick,
978 unlimited_liquidity(size_prec),
979 3,
980 );
981 add_order(
982 &mut book,
983 instrument,
984 OrderSide::Sell,
985 best_ask + tick,
986 unlimited_liquidity(size_prec),
987 4,
988 );
989 Ok(Some(book))
990 }
991}
992
993#[derive(Debug)]
996#[cfg_attr(
997 feature = "python",
998 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
999)]
1000#[cfg_attr(
1001 feature = "python",
1002 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1003)]
1004pub struct SizeAwareFillModel {
1005 state: ProbabilisticFillState,
1006}
1007
1008impl SizeAwareFillModel {
1009 pub fn new(
1015 prob_fill_on_limit: f64,
1016 prob_slippage: f64,
1017 random_seed: Option<u64>,
1018 ) -> anyhow::Result<Self> {
1019 Ok(Self {
1020 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1021 })
1022 }
1023}
1024
1025impl Clone for SizeAwareFillModel {
1026 fn clone(&self) -> Self {
1027 Self {
1028 state: self.state.clone(),
1029 }
1030 }
1031}
1032
1033impl Default for SizeAwareFillModel {
1034 fn default() -> Self {
1035 Self::new(1.0, 0.0, None).unwrap()
1036 }
1037}
1038
1039impl FillModel for SizeAwareFillModel {
1040 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1041 Ok(self.state.is_limit_filled())
1042 }
1043
1044 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1045 Ok(self.state.is_slipped())
1046 }
1047
1048 fn get_orderbook_for_fill_simulation(
1049 &mut self,
1050 instrument: &InstrumentAny,
1051 order: &OrderAny,
1052 best_bid: Option<Price>,
1053 best_ask: Option<Price>,
1054 ) -> anyhow::Result<Option<OrderBook>> {
1055 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
1056 return Ok(None);
1057 };
1058
1059 let tick = instrument.price_increment();
1060 let size_prec = instrument.size_precision();
1061 let mut book = build_l2_book(instrument.id());
1062
1063 let threshold = Quantity::new(10.0, size_prec);
1064 if order.quantity() <= threshold {
1065 add_order(
1067 &mut book,
1068 instrument,
1069 OrderSide::Buy,
1070 best_bid,
1071 Quantity::new(50.0, size_prec),
1072 1,
1073 );
1074 add_order(
1075 &mut book,
1076 instrument,
1077 OrderSide::Sell,
1078 best_ask,
1079 Quantity::new(50.0, size_prec),
1080 2,
1081 );
1082 } else {
1083 let remaining = order.quantity() - threshold;
1085 add_order(
1086 &mut book,
1087 instrument,
1088 OrderSide::Buy,
1089 best_bid,
1090 threshold,
1091 1,
1092 );
1093 add_order(
1094 &mut book,
1095 instrument,
1096 OrderSide::Sell,
1097 best_ask,
1098 threshold,
1099 2,
1100 );
1101 add_order(
1102 &mut book,
1103 instrument,
1104 OrderSide::Buy,
1105 best_bid - tick,
1106 remaining,
1107 3,
1108 );
1109 add_order(
1110 &mut book,
1111 instrument,
1112 OrderSide::Sell,
1113 best_ask + tick,
1114 remaining,
1115 4,
1116 );
1117 }
1118 Ok(Some(book))
1119 }
1120}
1121
1122#[derive(Debug)]
1124#[cfg_attr(
1125 feature = "python",
1126 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1127)]
1128#[cfg_attr(
1129 feature = "python",
1130 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1131)]
1132pub struct CompetitionAwareFillModel {
1133 state: ProbabilisticFillState,
1134 liquidity_factor: Decimal,
1135}
1136
1137impl CompetitionAwareFillModel {
1138 pub fn new(
1144 prob_fill_on_limit: f64,
1145 prob_slippage: f64,
1146 random_seed: Option<u64>,
1147 liquidity_factor: f64,
1148 ) -> anyhow::Result<Self> {
1149 let state = ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?;
1150 check_in_range_inclusive_f64(liquidity_factor, 0.0, 1.0, "liquidity_factor")?;
1151 let liquidity_factor = Decimal::try_from(liquidity_factor)?;
1152
1153 Ok(Self {
1154 state,
1155 liquidity_factor,
1156 })
1157 }
1158}
1159
1160impl Clone for CompetitionAwareFillModel {
1161 fn clone(&self) -> Self {
1162 Self {
1163 state: self.state.clone(),
1164 liquidity_factor: self.liquidity_factor,
1165 }
1166 }
1167}
1168
1169impl Default for CompetitionAwareFillModel {
1170 fn default() -> Self {
1171 Self::new(1.0, 0.0, None, 0.3).unwrap()
1172 }
1173}
1174
1175impl FillModel for CompetitionAwareFillModel {
1176 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1177 Ok(self.state.is_limit_filled())
1178 }
1179
1180 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1181 Ok(self.state.is_slipped())
1182 }
1183
1184 fn get_orderbook_for_fill_simulation(
1185 &mut self,
1186 instrument: &InstrumentAny,
1187 _order: &OrderAny,
1188 best_bid: Option<Price>,
1189 best_ask: Option<Price>,
1190 ) -> anyhow::Result<Option<OrderBook>> {
1191 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
1192 return Ok(None);
1193 };
1194
1195 let size_prec = instrument.size_precision();
1196 let mut book = build_l2_book(instrument.id());
1197
1198 let available = Quantity::from_decimal_dp(
1200 (dec!(1000) * self.liquidity_factor).max(Decimal::ONE),
1201 size_prec,
1202 )?;
1203
1204 add_order(
1205 &mut book,
1206 instrument,
1207 OrderSide::Buy,
1208 best_bid,
1209 available,
1210 1,
1211 );
1212 add_order(
1213 &mut book,
1214 instrument,
1215 OrderSide::Sell,
1216 best_ask,
1217 available,
1218 2,
1219 );
1220 Ok(Some(book))
1221 }
1222}
1223
1224#[derive(Debug)]
1227#[cfg_attr(
1228 feature = "python",
1229 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1230)]
1231#[cfg_attr(
1232 feature = "python",
1233 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1234)]
1235pub struct VolumeSensitiveFillModel {
1236 state: ProbabilisticFillState,
1237 recent_volume: f64,
1238}
1239
1240impl VolumeSensitiveFillModel {
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 recent_volume: 1000.0,
1254 })
1255 }
1256
1257 pub fn set_recent_volume(&mut self, volume: f64) {
1258 self.recent_volume = volume;
1259 }
1260}
1261
1262impl Clone for VolumeSensitiveFillModel {
1263 fn clone(&self) -> Self {
1264 Self {
1265 state: self.state.clone(),
1266 recent_volume: self.recent_volume,
1267 }
1268 }
1269}
1270
1271impl Default for VolumeSensitiveFillModel {
1272 fn default() -> Self {
1273 Self::new(1.0, 0.0, None).unwrap()
1274 }
1275}
1276
1277impl FillModel for VolumeSensitiveFillModel {
1278 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1279 Ok(self.state.is_limit_filled())
1280 }
1281
1282 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1283 Ok(self.state.is_slipped())
1284 }
1285
1286 fn get_orderbook_for_fill_simulation(
1287 &mut self,
1288 instrument: &InstrumentAny,
1289 _order: &OrderAny,
1290 best_bid: Option<Price>,
1291 best_ask: Option<Price>,
1292 ) -> anyhow::Result<Option<OrderBook>> {
1293 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
1294 return Ok(None);
1295 };
1296
1297 let tick = instrument.price_increment();
1298 let size_prec = instrument.size_precision();
1299 let mut book = build_l2_book(instrument.id());
1300
1301 check_non_negative_f64(self.recent_volume, "recent_volume")?;
1302 let recent_volume = Decimal::try_from(self.recent_volume)?;
1303
1304 let available =
1306 Quantity::from_decimal_dp((recent_volume * dec!(0.25)).max(Decimal::ONE), size_prec)?;
1307
1308 add_order(
1309 &mut book,
1310 instrument,
1311 OrderSide::Buy,
1312 best_bid,
1313 available,
1314 1,
1315 );
1316 add_order(
1317 &mut book,
1318 instrument,
1319 OrderSide::Sell,
1320 best_ask,
1321 available,
1322 2,
1323 );
1324 add_order(
1325 &mut book,
1326 instrument,
1327 OrderSide::Buy,
1328 best_bid - tick,
1329 unlimited_liquidity(size_prec),
1330 3,
1331 );
1332 add_order(
1333 &mut book,
1334 instrument,
1335 OrderSide::Sell,
1336 best_ask + tick,
1337 unlimited_liquidity(size_prec),
1338 4,
1339 );
1340 Ok(Some(book))
1341 }
1342}
1343
1344#[derive(Debug)]
1347#[cfg_attr(
1348 feature = "python",
1349 pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
1350)]
1351#[cfg_attr(
1352 feature = "python",
1353 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1354)]
1355pub struct MarketHoursFillModel {
1356 state: ProbabilisticFillState,
1357 is_low_liquidity: bool,
1358}
1359
1360impl MarketHoursFillModel {
1361 pub fn new(
1367 prob_fill_on_limit: f64,
1368 prob_slippage: f64,
1369 random_seed: Option<u64>,
1370 ) -> anyhow::Result<Self> {
1371 Ok(Self {
1372 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1373 is_low_liquidity: false,
1374 })
1375 }
1376
1377 pub fn set_low_liquidity_period(&mut self, is_low_liquidity: bool) {
1378 self.is_low_liquidity = is_low_liquidity;
1379 }
1380
1381 pub fn is_low_liquidity_period(&self) -> bool {
1382 self.is_low_liquidity
1383 }
1384}
1385
1386impl Clone for MarketHoursFillModel {
1387 fn clone(&self) -> Self {
1388 Self {
1389 state: self.state.clone(),
1390 is_low_liquidity: self.is_low_liquidity,
1391 }
1392 }
1393}
1394
1395impl Default for MarketHoursFillModel {
1396 fn default() -> Self {
1397 Self::new(1.0, 0.0, None).unwrap()
1398 }
1399}
1400
1401impl FillModel for MarketHoursFillModel {
1402 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1403 Ok(self.state.is_limit_filled())
1404 }
1405
1406 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1407 Ok(self.state.is_slipped())
1408 }
1409
1410 fn get_orderbook_for_fill_simulation(
1411 &mut self,
1412 instrument: &InstrumentAny,
1413 _order: &OrderAny,
1414 best_bid: Option<Price>,
1415 best_ask: Option<Price>,
1416 ) -> anyhow::Result<Option<OrderBook>> {
1417 let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) else {
1418 return Ok(None);
1419 };
1420
1421 let tick = instrument.price_increment();
1422 let size_prec = instrument.size_precision();
1423 let mut book = build_l2_book(instrument.id());
1424 let normal_volume = 500.0;
1425
1426 if self.is_low_liquidity {
1427 add_order(
1428 &mut book,
1429 instrument,
1430 OrderSide::Buy,
1431 best_bid - tick,
1432 Quantity::new(normal_volume, size_prec),
1433 1,
1434 );
1435 add_order(
1436 &mut book,
1437 instrument,
1438 OrderSide::Sell,
1439 best_ask + tick,
1440 Quantity::new(normal_volume, size_prec),
1441 2,
1442 );
1443 } else {
1444 add_order(
1445 &mut book,
1446 instrument,
1447 OrderSide::Buy,
1448 best_bid,
1449 Quantity::new(normal_volume, size_prec),
1450 1,
1451 );
1452 add_order(
1453 &mut book,
1454 instrument,
1455 OrderSide::Sell,
1456 best_ask,
1457 Quantity::new(normal_volume, size_prec),
1458 2,
1459 );
1460 }
1461 Ok(Some(book))
1462 }
1463}
1464
1465#[derive(Clone, Debug)]
1466pub enum FillModelAny {
1467 Default(DefaultFillModel),
1468 BestPrice(BestPriceFillModel),
1469 OneTickSlippage(OneTickSlippageFillModel),
1470 Probabilistic(ProbabilisticFillModel),
1471 TwoTier(TwoTierFillModel),
1472 ThreeTier(ThreeTierFillModel),
1473 LimitOrderPartialFill(LimitOrderPartialFillModel),
1474 SizeAware(SizeAwareFillModel),
1475 CompetitionAware(CompetitionAwareFillModel),
1476 VolumeSensitive(VolumeSensitiveFillModel),
1477 MarketHours(MarketHoursFillModel),
1478}
1479
1480impl FillModel for FillModelAny {
1481 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1482 match self {
1483 Self::Default(m) => m.is_limit_filled(),
1484 Self::BestPrice(m) => m.is_limit_filled(),
1485 Self::OneTickSlippage(m) => m.is_limit_filled(),
1486 Self::Probabilistic(m) => m.is_limit_filled(),
1487 Self::TwoTier(m) => m.is_limit_filled(),
1488 Self::ThreeTier(m) => m.is_limit_filled(),
1489 Self::LimitOrderPartialFill(m) => m.is_limit_filled(),
1490 Self::SizeAware(m) => m.is_limit_filled(),
1491 Self::CompetitionAware(m) => m.is_limit_filled(),
1492 Self::VolumeSensitive(m) => m.is_limit_filled(),
1493 Self::MarketHours(m) => m.is_limit_filled(),
1494 }
1495 }
1496
1497 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
1498 match self {
1499 Self::Default(m) => m.fill_limit_inside_spread(),
1500 Self::BestPrice(m) => m.fill_limit_inside_spread(),
1501 Self::OneTickSlippage(m) => m.fill_limit_inside_spread(),
1502 Self::Probabilistic(m) => m.fill_limit_inside_spread(),
1503 Self::TwoTier(m) => m.fill_limit_inside_spread(),
1504 Self::ThreeTier(m) => m.fill_limit_inside_spread(),
1505 Self::LimitOrderPartialFill(m) => m.fill_limit_inside_spread(),
1506 Self::SizeAware(m) => m.fill_limit_inside_spread(),
1507 Self::CompetitionAware(m) => m.fill_limit_inside_spread(),
1508 Self::VolumeSensitive(m) => m.fill_limit_inside_spread(),
1509 Self::MarketHours(m) => m.fill_limit_inside_spread(),
1510 }
1511 }
1512
1513 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1514 match self {
1515 Self::Default(m) => m.is_slipped(),
1516 Self::BestPrice(m) => m.is_slipped(),
1517 Self::OneTickSlippage(m) => m.is_slipped(),
1518 Self::Probabilistic(m) => m.is_slipped(),
1519 Self::TwoTier(m) => m.is_slipped(),
1520 Self::ThreeTier(m) => m.is_slipped(),
1521 Self::LimitOrderPartialFill(m) => m.is_slipped(),
1522 Self::SizeAware(m) => m.is_slipped(),
1523 Self::CompetitionAware(m) => m.is_slipped(),
1524 Self::VolumeSensitive(m) => m.is_slipped(),
1525 Self::MarketHours(m) => m.is_slipped(),
1526 }
1527 }
1528
1529 #[rustfmt::skip]
1530 fn get_orderbook_for_fill_simulation(
1531 &mut self,
1532 instrument: &InstrumentAny,
1533 order: &OrderAny,
1534 best_bid: Option<Price>,
1535 best_ask: Option<Price>,
1536 ) -> anyhow::Result<Option<OrderBook>> {
1537 match self {
1538 Self::Default(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1539 Self::BestPrice(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1540 Self::OneTickSlippage(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1541 Self::Probabilistic(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1542 Self::TwoTier(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1543 Self::ThreeTier(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1544 Self::LimitOrderPartialFill(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1545 Self::SizeAware(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1546 Self::CompetitionAware(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1547 Self::VolumeSensitive(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1548 Self::MarketHours(m) => m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask),
1549 }
1550 }
1551}
1552
1553impl Default for FillModelAny {
1554 fn default() -> Self {
1555 Self::Default(DefaultFillModel::default())
1556 }
1557}
1558
1559impl Display for FillModelAny {
1560 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1561 match self {
1562 Self::Default(m) => write!(f, "{m}"),
1563 Self::BestPrice(_) => write!(f, "BestPriceFillModel"),
1564 Self::OneTickSlippage(_) => write!(f, "OneTickSlippageFillModel"),
1565 Self::Probabilistic(_) => write!(f, "ProbabilisticFillModel"),
1566 Self::TwoTier(_) => write!(f, "TwoTierFillModel"),
1567 Self::ThreeTier(_) => write!(f, "ThreeTierFillModel"),
1568 Self::LimitOrderPartialFill(_) => write!(f, "LimitOrderPartialFillModel"),
1569 Self::SizeAware(_) => write!(f, "SizeAwareFillModel"),
1570 Self::CompetitionAware(_) => write!(f, "CompetitionAwareFillModel"),
1571 Self::VolumeSensitive(_) => write!(f, "VolumeSensitiveFillModel"),
1572 Self::MarketHours(_) => write!(f, "MarketHoursFillModel"),
1573 }
1574 }
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use nautilus_core::correctness::CorrectnessError;
1580 use nautilus_model::{
1581 enums::OrderType,
1582 instruments::stubs::{
1583 audusd_sim, binary_option, crypto_perpetual_ethusdt, futures_contract_es,
1584 futures_spread_es,
1585 },
1586 orders::builder::OrderTestBuilder,
1587 };
1588 use rstest::{fixture, rstest};
1589
1590 use super::*;
1591
1592 #[fixture]
1593 fn fill_model() -> DefaultFillModel {
1594 let seed = 42;
1595 DefaultFillModel::new(0.5, 0.1, Some(seed)).unwrap()
1596 }
1597
1598 #[rstest]
1599 fn test_fill_model_display(fill_model: DefaultFillModel) {
1600 assert_eq!(
1601 format!("{fill_model}"),
1602 "DefaultFillModel(prob_fill_on_limit=0.5, prob_slippage=0.1)"
1603 );
1604 }
1605
1606 #[rstest]
1607 fn test_fill_model_param_prob_fill_on_limit_error() {
1608 let error = DefaultFillModel::new(1.1, 0.1, None).unwrap_err();
1609
1610 assert_eq!(
1611 error.downcast_ref::<CorrectnessError>(),
1612 Some(&CorrectnessError::OutOfRange {
1613 param: "prob_fill_on_limit".to_string(),
1614 min: "0".to_string(),
1615 max: "1".to_string(),
1616 value: "1.1".to_string(),
1617 type_name: "f64",
1618 })
1619 );
1620 assert_eq!(
1621 error.to_string(),
1622 "invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
1623 );
1624 }
1625
1626 #[rstest]
1627 fn test_fill_model_param_prob_slippage_error() {
1628 let error = DefaultFillModel::new(0.5, 1.1, None).unwrap_err();
1629
1630 assert_eq!(
1631 error.downcast_ref::<CorrectnessError>(),
1632 Some(&CorrectnessError::OutOfRange {
1633 param: "prob_slippage".to_string(),
1634 min: "0".to_string(),
1635 max: "1".to_string(),
1636 value: "1.1".to_string(),
1637 type_name: "f64",
1638 })
1639 );
1640 assert_eq!(
1641 error.to_string(),
1642 "invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
1643 );
1644 }
1645
1646 #[rstest]
1647 #[case(f64::NAN, "NaN")]
1648 #[case(f64::INFINITY, "inf")]
1649 #[case(f64::NEG_INFINITY, "-inf")]
1650 fn test_competition_aware_fill_model_rejects_non_finite_liquidity_factor(
1651 #[case] value: f64,
1652 #[case] expected_value: &str,
1653 ) {
1654 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1655
1656 assert_eq!(
1657 error.downcast_ref::<CorrectnessError>(),
1658 Some(&CorrectnessError::InvalidValue {
1659 param: "liquidity_factor".to_string(),
1660 value: expected_value.to_string(),
1661 type_name: "f64",
1662 })
1663 );
1664 }
1665
1666 #[rstest]
1667 #[case(-0.1, "-0.1")]
1668 #[case(1.1, "1.1")]
1669 fn test_competition_aware_fill_model_rejects_out_of_range_liquidity_factor(
1670 #[case] value: f64,
1671 #[case] expected_value: &str,
1672 ) {
1673 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1674
1675 assert_eq!(
1676 error.downcast_ref::<CorrectnessError>(),
1677 Some(&CorrectnessError::OutOfRange {
1678 param: "liquidity_factor".to_string(),
1679 min: "0".to_string(),
1680 max: "1".to_string(),
1681 value: expected_value.to_string(),
1682 type_name: "f64",
1683 })
1684 );
1685 }
1686
1687 #[rstest]
1688 #[case(f64::NAN, "NaN")]
1689 #[case(f64::INFINITY, "inf")]
1690 #[case(f64::NEG_INFINITY, "-inf")]
1691 fn test_volume_sensitive_fill_model_rejects_non_finite_volume(
1692 #[case] volume: f64,
1693 #[case] expected_value: &str,
1694 ) {
1695 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1696 let order = OrderTestBuilder::new(OrderType::Market)
1697 .instrument_id(instrument.id())
1698 .side(OrderSide::Buy)
1699 .quantity(Quantity::from(100_000))
1700 .build();
1701 let mut model = VolumeSensitiveFillModel::default();
1702 model.set_recent_volume(volume);
1703
1704 let error = model
1705 .get_orderbook_for_fill_simulation(
1706 &instrument,
1707 &order,
1708 Some(Price::from("0.80000")),
1709 Some(Price::from("0.80010")),
1710 )
1711 .unwrap_err();
1712
1713 assert_eq!(
1714 error.downcast_ref::<CorrectnessError>(),
1715 Some(&CorrectnessError::InvalidValue {
1716 param: "recent_volume".to_string(),
1717 value: expected_value.to_string(),
1718 type_name: "f64",
1719 })
1720 );
1721 }
1722
1723 #[rstest]
1724 fn test_volume_sensitive_fill_model_rejects_negative_volume() {
1725 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1726 let order = OrderTestBuilder::new(OrderType::Market)
1727 .instrument_id(instrument.id())
1728 .side(OrderSide::Buy)
1729 .quantity(Quantity::from(100_000))
1730 .build();
1731 let mut model = VolumeSensitiveFillModel::default();
1732 model.set_recent_volume(-1.0);
1733
1734 let error = model
1735 .get_orderbook_for_fill_simulation(
1736 &instrument,
1737 &order,
1738 Some(Price::from("0.80000")),
1739 Some(Price::from("0.80010")),
1740 )
1741 .unwrap_err();
1742
1743 assert_eq!(
1744 error.downcast_ref::<CorrectnessError>(),
1745 Some(&CorrectnessError::NegativeValue {
1746 param: "recent_volume".to_string(),
1747 value: "-1".to_string(),
1748 type_name: "f64",
1749 })
1750 );
1751 }
1752
1753 #[rstest]
1754 fn test_volume_sensitive_fill_model_rejects_volume_above_quantity_range() {
1755 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1756 let order = OrderTestBuilder::new(OrderType::Market)
1757 .instrument_id(instrument.id())
1758 .side(OrderSide::Buy)
1759 .quantity(Quantity::from(100_000))
1760 .build();
1761 let mut model = VolumeSensitiveFillModel::default();
1762 model.set_recent_volume(100_000_000_000_000_000.0);
1763
1764 let error = model
1765 .get_orderbook_for_fill_simulation(
1766 &instrument,
1767 &order,
1768 Some(Price::from("0.80000")),
1769 Some(Price::from("0.80010")),
1770 )
1771 .unwrap_err();
1772
1773 assert!(matches!(
1774 error.downcast_ref::<CorrectnessError>(),
1775 Some(CorrectnessError::PredicateViolation { message })
1776 if message.contains("QuantityRaw") || message.contains("QUANTITY_RAW_MAX")
1777 ));
1778 }
1779
1780 #[rstest]
1781 #[case(0.0, Quantity::from(1))]
1782 #[case(0.5, Quantity::from(500))]
1783 #[case(1.0, Quantity::from(1_000))]
1784 fn test_competition_aware_fill_model_builds_expected_liquidity(
1785 #[case] liquidity_factor: f64,
1786 #[case] expected_size: Quantity,
1787 ) {
1788 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1789 let order = OrderTestBuilder::new(OrderType::Market)
1790 .instrument_id(instrument.id())
1791 .side(OrderSide::Buy)
1792 .quantity(Quantity::from(100_000))
1793 .build();
1794 let best_bid = Price::from("0.80000");
1795 let best_ask = Price::from("0.80010");
1796 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, liquidity_factor).unwrap();
1797
1798 let book = model
1799 .get_orderbook_for_fill_simulation(&instrument, &order, Some(best_bid), Some(best_ask))
1800 .unwrap()
1801 .unwrap();
1802
1803 assert_eq!(book.best_bid_price(), Some(best_bid));
1804 assert_eq!(book.best_ask_price(), Some(best_ask));
1805 assert_eq!(book.best_bid_size(), Some(expected_size));
1806 assert_eq!(book.best_ask_size(), Some(expected_size));
1807 }
1808
1809 #[rstest]
1810 fn test_competition_aware_fill_model_preserves_instrument_size_precision() {
1811 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1812 let order = OrderTestBuilder::new(OrderType::Market)
1813 .instrument_id(instrument.id())
1814 .side(OrderSide::Buy)
1815 .quantity(Quantity::from(100_000))
1816 .build();
1817 let best_bid = Price::from("2000.00");
1818 let best_ask = Price::from("2000.01");
1819 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, 0.001234).unwrap();
1820
1821 let book = model
1822 .get_orderbook_for_fill_simulation(&instrument, &order, Some(best_bid), Some(best_ask))
1823 .unwrap()
1824 .unwrap();
1825
1826 assert_eq!(book.best_bid_price(), Some(best_bid));
1827 assert_eq!(book.best_ask_price(), Some(best_ask));
1828 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.234")));
1829 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.234")));
1830 }
1831
1832 #[rstest]
1833 fn test_volume_sensitive_fill_model_builds_expected_liquidity() {
1834 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1835 let order = OrderTestBuilder::new(OrderType::Market)
1836 .instrument_id(instrument.id())
1837 .side(OrderSide::Buy)
1838 .quantity(Quantity::from(100_000))
1839 .build();
1840 let best_bid = Price::from("2000.00");
1841 let best_ask = Price::from("2000.01");
1842 let mut model = VolumeSensitiveFillModel::default();
1843 model.set_recent_volume(5.678);
1844
1845 let book = model
1846 .get_orderbook_for_fill_simulation(&instrument, &order, Some(best_bid), Some(best_ask))
1847 .unwrap()
1848 .unwrap();
1849
1850 assert_eq!(book.best_bid_price(), Some(best_bid));
1851 assert_eq!(book.best_ask_price(), Some(best_ask));
1852 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.420")));
1853 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.420")));
1854 }
1855
1856 #[rstest]
1857 fn test_fill_model_is_limit_filled(mut fill_model: DefaultFillModel) {
1858 let result = fill_model.is_limit_filled().unwrap();
1860 assert!(!result);
1861 }
1862
1863 #[rstest]
1864 fn test_fill_model_is_slipped(mut fill_model: DefaultFillModel) {
1865 let result = fill_model.is_slipped().unwrap();
1867 assert!(!result);
1868 }
1869
1870 #[rstest]
1871 fn test_default_fill_model_returns_none() {
1872 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1873 let order = OrderTestBuilder::new(OrderType::Market)
1874 .instrument_id(instrument.id())
1875 .side(OrderSide::Buy)
1876 .quantity(Quantity::from(100_000))
1877 .build();
1878
1879 let mut model = DefaultFillModel::default();
1880 let result = model
1881 .get_orderbook_for_fill_simulation(
1882 &instrument,
1883 &order,
1884 Some(Price::from("0.80000")),
1885 Some(Price::from("0.80010")),
1886 )
1887 .unwrap();
1888 assert!(result.is_none());
1889 }
1890
1891 #[rstest]
1892 fn test_best_price_fill_model_returns_book() {
1893 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1894 let order = OrderTestBuilder::new(OrderType::Market)
1895 .instrument_id(instrument.id())
1896 .side(OrderSide::Buy)
1897 .quantity(Quantity::from(100_000))
1898 .build();
1899
1900 let mut model = BestPriceFillModel::default();
1901 let result = model
1902 .get_orderbook_for_fill_simulation(
1903 &instrument,
1904 &order,
1905 Some(Price::from("0.80000")),
1906 Some(Price::from("0.80010")),
1907 )
1908 .unwrap();
1909 assert!(result.is_some());
1910 let book = result.unwrap();
1911 assert_eq!(book.best_bid_price().unwrap(), Price::from("0.80000"));
1912 assert_eq!(book.best_ask_price().unwrap(), Price::from("0.80010"));
1913 }
1914
1915 #[rstest]
1916 #[case::buy_inside_spread(OrderSide::Buy, "0.80005", Some(("0.80000", "0.80005")))]
1917 #[case::buy_at_bid(OrderSide::Buy, "0.80000", Some(("0.80000", "0.80000")))]
1918 #[case::buy_crossing(OrderSide::Buy, "0.80020", Some(("0.80000", "0.80010")))]
1919 #[case::buy_passive(OrderSide::Buy, "0.79990", None)]
1920 #[case::sell_inside_spread(OrderSide::Sell, "0.80005", Some(("0.80005", "0.80010")))]
1921 #[case::sell_at_ask(OrderSide::Sell, "0.80010", Some(("0.80010", "0.80010")))]
1922 #[case::sell_crossing(OrderSide::Sell, "0.79990", Some(("0.80000", "0.80010")))]
1923 #[case::sell_passive(OrderSide::Sell, "0.80020", None)]
1924 fn test_best_price_fill_model_limit_book(
1925 #[case] side: OrderSide,
1926 #[case] limit_price: &str,
1927 #[case] expected_touch: Option<(&str, &str)>,
1928 ) {
1929 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1930 let order = OrderTestBuilder::new(OrderType::Limit)
1931 .instrument_id(instrument.id())
1932 .side(side)
1933 .price(Price::from(limit_price))
1934 .quantity(Quantity::from(100_000))
1935 .build();
1936
1937 let mut model = BestPriceFillModel::default();
1938 let book = model
1939 .get_orderbook_for_fill_simulation(
1940 &instrument,
1941 &order,
1942 Some(Price::from("0.80000")),
1943 Some(Price::from("0.80010")),
1944 )
1945 .unwrap();
1946
1947 let touch = book.map(|book| {
1948 (
1949 book.best_bid_price().unwrap(),
1950 book.best_ask_price().unwrap(),
1951 )
1952 });
1953
1954 let expected_touch = expected_touch.map(|(bid, ask)| (Price::from(bid), Price::from(ask)));
1955 assert_eq!(touch, expected_touch);
1956 }
1957
1958 #[rstest]
1959 #[case::at_min("0.001", true)]
1960 #[case::at_max("0.999", true)]
1961 #[case::below_min("0.000", false)]
1962 #[case::above_max("1.000", false)]
1963 fn test_is_price_tradable_respects_price_limits(#[case] price: &str, #[case] expected: bool) {
1964 let mut option = binary_option();
1965 option.min_price = Some(Price::from("0.001"));
1966 option.max_price = Some(Price::from("0.999"));
1967 let instrument = InstrumentAny::BinaryOption(option);
1968
1969 assert_eq!(is_price_tradable(&instrument, Price::from(price)), expected);
1970 }
1971
1972 #[rstest]
1973 #[case::zero_disallowed(InstrumentAny::BinaryOption(binary_option()), "0.000", false)]
1974 #[case::negative_disallowed(InstrumentAny::BinaryOption(binary_option()), "-0.001", false)]
1975 #[case::negative_allowed(InstrumentAny::FuturesSpread(futures_spread_es()), "-1.00", true)]
1976 #[case::negative_allowed_futures(
1977 InstrumentAny::FuturesContract(futures_contract_es(None, None)),
1978 "-1.00",
1979 true
1980 )]
1981 fn test_is_price_tradable_respects_price_sign(
1982 #[case] instrument: InstrumentAny,
1983 #[case] price: &str,
1984 #[case] expected: bool,
1985 ) {
1986 assert_eq!(is_price_tradable(&instrument, Price::from(price)), expected);
1987 }
1988
1989 #[rstest]
1990 fn test_one_tick_slippage_fill_model_skips_levels_outside_price_limits() {
1991 let mut option = binary_option();
1992 option.min_price = Some(Price::from("0.001"));
1993 option.max_price = Some(Price::from("0.999"));
1994 let instrument = InstrumentAny::BinaryOption(option);
1995 let order = OrderTestBuilder::new(OrderType::Market)
1996 .instrument_id(instrument.id())
1997 .side(OrderSide::Sell)
1998 .quantity(Quantity::from("10.00"))
1999 .build();
2000
2001 let mut model = OneTickSlippageFillModel::default();
2002 let book = model
2003 .get_orderbook_for_fill_simulation(
2004 &instrument,
2005 &order,
2006 Some(Price::from("0.001")),
2007 Some(Price::from("0.999")),
2008 )
2009 .unwrap()
2010 .unwrap();
2011
2012 assert_eq!(book.best_bid_price(), None);
2013 assert_eq!(book.best_ask_price(), None);
2014 }
2015
2016 #[rstest]
2017 fn test_fill_model_tolerates_probabilities_within_validation_epsilon() {
2018 let mut model =
2019 DefaultFillModel::new(1.0 + f64::EPSILON, 0.3 - 0.1 - 0.2, Some(1)).unwrap();
2020
2021 assert!(model.is_limit_filled().unwrap());
2022 assert!(!model.is_slipped().unwrap());
2023 }
2024
2025 #[rstest]
2026 fn test_one_tick_slippage_fill_model() {
2027 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2028 let order = OrderTestBuilder::new(OrderType::Market)
2029 .instrument_id(instrument.id())
2030 .side(OrderSide::Buy)
2031 .quantity(Quantity::from(100_000))
2032 .build();
2033
2034 let tick = instrument.price_increment();
2035 let best_bid = Price::from("0.80000");
2036 let best_ask = Price::from("0.80010");
2037
2038 let mut model = OneTickSlippageFillModel::default();
2039 let result = model
2040 .get_orderbook_for_fill_simulation(&instrument, &order, Some(best_bid), Some(best_ask))
2041 .unwrap();
2042 assert!(result.is_some());
2043 let book = result.unwrap();
2044
2045 assert_eq!(book.best_bid_price().unwrap(), best_bid - tick);
2046 assert_eq!(book.best_ask_price().unwrap(), best_ask + tick);
2047 }
2048
2049 #[rstest]
2050 fn test_fill_model_any_dispatch() {
2051 let model = FillModelAny::default();
2052 assert!(matches!(model, FillModelAny::Default(_)));
2053 }
2054
2055 #[rstest]
2056 fn test_fill_model_any_is_limit_filled() {
2057 let mut model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.1, Some(42)).unwrap());
2058 let result = model.is_limit_filled().unwrap();
2059 assert!(!result);
2060 }
2061
2062 #[rstest]
2063 fn test_fill_model_handle_from_any_owns_state_per_conversion() {
2064 let model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.0, Some(42)).unwrap());
2065 let mut expected_model = model.clone();
2066 let mut first: FillModelHandle = model.clone().into();
2067 let mut second: FillModelHandle = model.into();
2068
2069 let expected: Vec<_> = (0..16)
2070 .map(|_| expected_model.is_limit_filled().unwrap())
2071 .collect();
2072 let first_results: Vec<_> = (0..16).map(|_| first.is_limit_filled().unwrap()).collect();
2073 let second_results: Vec<_> = (0..16).map(|_| second.is_limit_filled().unwrap()).collect();
2074 let has_variation = expected.windows(2).any(|window| window[0] != window[1]);
2075
2076 assert!(has_variation);
2077 assert_eq!(first_results, expected);
2078 assert_eq!(second_results, expected);
2079 }
2080
2081 #[rstest]
2082 fn test_default_fill_model_fill_limit_inside_spread_is_false() {
2083 let model = DefaultFillModel::default();
2084 assert!(!model.fill_limit_inside_spread().unwrap());
2085 }
2086
2087 #[rstest]
2088 #[case(None, None)]
2089 #[case(None, Some(Price::from("0.80010")))]
2090 #[case(Some(Price::from("0.80000")), None)]
2091 fn test_builtin_fill_models_delegate_when_quotes_are_missing(
2092 #[case] best_bid: Option<Price>,
2093 #[case] best_ask: Option<Price>,
2094 ) {
2095 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2096 let order = OrderTestBuilder::new(OrderType::Market)
2097 .instrument_id(instrument.id())
2098 .side(OrderSide::Buy)
2099 .quantity(Quantity::from(10))
2100 .build();
2101 let models: Vec<Box<dyn FillModel>> = vec![
2102 Box::new(DefaultFillModel::default()),
2103 Box::new(BestPriceFillModel::default()),
2104 Box::new(OneTickSlippageFillModel::default()),
2105 Box::new(ProbabilisticFillModel::default()),
2106 Box::new(TwoTierFillModel::default()),
2107 Box::new(ThreeTierFillModel::default()),
2108 Box::new(LimitOrderPartialFillModel::default()),
2109 Box::new(SizeAwareFillModel::default()),
2110 Box::new(CompetitionAwareFillModel::default()),
2111 Box::new(VolumeSensitiveFillModel::default()),
2112 Box::new(MarketHoursFillModel::default()),
2113 ];
2114
2115 for mut model in models {
2116 assert!(
2117 model
2118 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
2119 .unwrap()
2120 .is_none()
2121 );
2122 }
2123 }
2124
2125 #[rstest]
2126 fn test_best_price_fill_model_fill_limit_inside_spread_is_true() {
2127 let model = BestPriceFillModel::default();
2128 assert!(model.fill_limit_inside_spread().unwrap());
2129 }
2130
2131 #[rstest]
2132 fn test_one_tick_slippage_fill_model_fill_limit_inside_spread_is_false() {
2133 let model = OneTickSlippageFillModel::default();
2134 assert!(!model.fill_limit_inside_spread().unwrap());
2135 }
2136
2137 #[rstest]
2138 fn test_fill_model_any_fill_limit_inside_spread_dispatch() {
2139 let default = FillModelAny::Default(DefaultFillModel::default());
2140 assert!(!default.fill_limit_inside_spread().unwrap());
2141
2142 let best_price = FillModelAny::BestPrice(BestPriceFillModel::default());
2143 assert!(best_price.fill_limit_inside_spread().unwrap());
2144
2145 let one_tick = FillModelAny::OneTickSlippage(OneTickSlippageFillModel::default());
2146 assert!(!one_tick.fill_limit_inside_spread().unwrap());
2147 }
2148
2149 #[rstest]
2150 fn test_market_hours_fill_model_switches_liquidity_and_preserves_clone_state() {
2151 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
2152 let order = OrderTestBuilder::new(OrderType::Market)
2153 .instrument_id(instrument.id())
2154 .side(OrderSide::Buy)
2155 .quantity(Quantity::from(17))
2156 .build();
2157 let mut model = MarketHoursFillModel::default();
2158
2159 for (low_liquidity, bid, ask) in [
2160 (false, dec!(0.80000), dec!(0.80010)),
2161 (true, dec!(0.79999), dec!(0.80011)),
2162 (false, dec!(0.80000), dec!(0.80010)),
2163 ] {
2164 model.set_low_liquidity_period(low_liquidity);
2165 let mut cloned = model.clone();
2166 let book = cloned
2167 .get_orderbook_for_fill_simulation(
2168 &instrument,
2169 &order,
2170 Some(Price::from("0.80000")),
2171 Some(Price::from("0.80010")),
2172 )
2173 .unwrap()
2174 .unwrap();
2175
2176 assert_eq!(model.is_low_liquidity_period(), low_liquidity);
2177 assert_eq!(cloned.is_low_liquidity_period(), low_liquidity);
2178 assert_eq!(
2179 book.bids_as_map(None).into_iter().collect::<Vec<_>>(),
2180 vec![(bid, dec!(500))]
2181 );
2182 assert_eq!(
2183 book.asks_as_map(None).into_iter().collect::<Vec<_>>(),
2184 vec![(ask, dec!(500))]
2185 );
2186 }
2187 }
2188}