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