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(
88 &mut self,
89 instrument: &InstrumentAny,
90 order: &OrderAny,
91 best_bid: Price,
92 best_ask: Price,
93 ) -> anyhow::Result<Option<OrderBook>>;
94}
95
96#[derive(Clone)]
98pub struct FillModelHandle(Rc<RefCell<dyn FillModel>>);
99
100impl FillModelHandle {
101 #[must_use]
103 pub fn new<T>(model: T) -> Self
104 where
105 T: FillModel + 'static,
106 {
107 Self(Rc::new(RefCell::new(model)))
108 }
109
110 #[must_use]
112 pub fn from_rc(model: Rc<RefCell<dyn FillModel>>) -> Self {
113 Self(model)
114 }
115}
116
117impl Debug for FillModelHandle {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_tuple(stringify!(FillModelHandle))
120 .field(&"<dyn FillModel>")
121 .finish()
122 }
123}
124
125impl FillModel for FillModelHandle {
126 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
127 self.0.borrow_mut().is_limit_filled()
128 }
129
130 fn is_slipped(&mut self) -> anyhow::Result<bool> {
131 self.0.borrow_mut().is_slipped()
132 }
133
134 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
135 self.0.borrow().fill_limit_inside_spread()
136 }
137
138 fn get_orderbook_for_fill_simulation(
139 &mut self,
140 instrument: &InstrumentAny,
141 order: &OrderAny,
142 best_bid: Price,
143 best_ask: Price,
144 ) -> anyhow::Result<Option<OrderBook>> {
145 self.0
146 .borrow_mut()
147 .get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
148 }
149}
150
151impl Default for FillModelHandle {
152 fn default() -> Self {
153 FillModelAny::default().into()
154 }
155}
156
157impl From<FillModelAny> for FillModelHandle {
158 fn from(model: FillModelAny) -> Self {
159 Self::new(model)
160 }
161}
162
163#[derive(Debug)]
164pub struct ProbabilisticFillState {
165 prob_fill_on_limit: f64,
166 prob_slippage: f64,
167 random_seed: Option<u64>,
168 rng: StdRng,
169}
170
171impl ProbabilisticFillState {
172 pub fn new(
178 prob_fill_on_limit: f64,
179 prob_slippage: f64,
180 random_seed: Option<u64>,
181 ) -> anyhow::Result<Self> {
182 check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")?;
183 check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage")?;
184 let rng = match random_seed {
185 Some(seed) => StdRng::seed_from_u64(seed),
186 None => default_std_rng(),
187 };
188 Ok(Self {
189 prob_fill_on_limit,
190 prob_slippage,
191 random_seed,
192 rng,
193 })
194 }
195
196 pub fn is_limit_filled(&mut self) -> bool {
197 self.event_success(self.prob_fill_on_limit)
198 }
199
200 pub fn is_slipped(&mut self) -> bool {
201 self.event_success(self.prob_slippage)
202 }
203
204 pub fn random_bool(&mut self, probability: f64) -> bool {
205 self.event_success(probability)
206 }
207
208 fn event_success(&mut self, probability: f64) -> bool {
209 match probability {
210 0.0 => false,
211 1.0 => true,
212 _ => self.rng.random_bool(probability),
213 }
214 }
215}
216
217impl Clone for ProbabilisticFillState {
218 fn clone(&self) -> Self {
219 Self::new(
220 self.prob_fill_on_limit,
221 self.prob_slippage,
222 self.random_seed,
223 )
224 .expect("ProbabilisticFillState clone should not fail with valid parameters")
225 }
226}
227
228fn default_std_rng() -> StdRng {
229 #[cfg(all(feature = "simulation", madsim))]
230 {
231 if madsim::runtime::Handle::try_current().is_ok() {
236 let mut seed = [0u8; 32];
237 madsim::rand::thread_rng().fill_bytes(&mut seed);
238 return StdRng::from_seed(seed);
239 }
240 }
241
242 StdRng::from_rng(&mut rand::rng()) }
244
245fn build_l2_book(instrument_id: InstrumentId) -> OrderBook {
246 OrderBook::new(instrument_id, BookType::L2_MBP)
247}
248
249fn add_order(book: &mut OrderBook, side: OrderSide, price: Price, size: Quantity, order_id: u64) {
250 let order = BookOrder::new(side, price, size, order_id);
251 book.add(order, 0, 0, UnixNanos::default());
252}
253
254#[derive(Debug)]
255#[cfg_attr(
256 feature = "python",
257 pyo3::pyclass(
258 module = "nautilus_trader.core.nautilus_pyo3.execution",
259 unsendable,
260 from_py_object
261 )
262)]
263#[cfg_attr(
264 feature = "python",
265 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
266)]
267pub struct DefaultFillModel {
268 state: ProbabilisticFillState,
269}
270
271impl DefaultFillModel {
272 pub fn new(
278 prob_fill_on_limit: f64,
279 prob_slippage: f64,
280 random_seed: Option<u64>,
281 ) -> anyhow::Result<Self> {
282 Ok(Self {
283 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
284 })
285 }
286}
287
288impl Clone for DefaultFillModel {
289 fn clone(&self) -> Self {
290 Self {
291 state: self.state.clone(),
292 }
293 }
294}
295
296impl Default for DefaultFillModel {
297 fn default() -> Self {
298 Self::new(1.0, 0.0, None).unwrap()
299 }
300}
301
302impl Display for DefaultFillModel {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 write!(
305 f,
306 "DefaultFillModel(prob_fill_on_limit: {}, prob_slippage: {})",
307 self.state.prob_fill_on_limit, self.state.prob_slippage
308 )
309 }
310}
311
312impl FillModel for DefaultFillModel {
313 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
314 Ok(self.state.is_limit_filled())
315 }
316
317 fn is_slipped(&mut self) -> anyhow::Result<bool> {
318 Ok(self.state.is_slipped())
319 }
320
321 fn get_orderbook_for_fill_simulation(
322 &mut self,
323 _instrument: &InstrumentAny,
324 _order: &OrderAny,
325 _best_bid: Price,
326 _best_ask: Price,
327 ) -> anyhow::Result<Option<OrderBook>> {
328 Ok(None)
329 }
330}
331
332#[derive(Debug)]
334#[cfg_attr(
335 feature = "python",
336 pyo3::pyclass(
337 module = "nautilus_trader.core.nautilus_pyo3.execution",
338 unsendable,
339 from_py_object
340 )
341)]
342#[cfg_attr(
343 feature = "python",
344 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
345)]
346pub struct BestPriceFillModel {
347 state: ProbabilisticFillState,
348}
349
350impl BestPriceFillModel {
351 pub fn new(
357 prob_fill_on_limit: f64,
358 prob_slippage: f64,
359 random_seed: Option<u64>,
360 ) -> anyhow::Result<Self> {
361 Ok(Self {
362 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
363 })
364 }
365}
366
367impl Clone for BestPriceFillModel {
368 fn clone(&self) -> Self {
369 Self {
370 state: self.state.clone(),
371 }
372 }
373}
374
375impl Default for BestPriceFillModel {
376 fn default() -> Self {
377 Self::new(1.0, 0.0, None).unwrap()
378 }
379}
380
381impl FillModel for BestPriceFillModel {
382 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
383 Ok(self.state.is_limit_filled())
384 }
385
386 fn is_slipped(&mut self) -> anyhow::Result<bool> {
387 Ok(self.state.is_slipped())
388 }
389
390 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
391 Ok(true)
392 }
393
394 fn get_orderbook_for_fill_simulation(
395 &mut self,
396 instrument: &InstrumentAny,
397 _order: &OrderAny,
398 best_bid: Price,
399 best_ask: Price,
400 ) -> anyhow::Result<Option<OrderBook>> {
401 let mut book = build_l2_book(instrument.id());
402 let size_prec = instrument.size_precision();
403 add_order(
404 &mut book,
405 OrderSide::Buy,
406 best_bid,
407 unlimited_liquidity(size_prec),
408 1,
409 );
410 add_order(
411 &mut book,
412 OrderSide::Sell,
413 best_ask,
414 unlimited_liquidity(size_prec),
415 2,
416 );
417 Ok(Some(book))
418 }
419}
420
421#[derive(Debug)]
423#[cfg_attr(
424 feature = "python",
425 pyo3::pyclass(
426 module = "nautilus_trader.core.nautilus_pyo3.execution",
427 unsendable,
428 from_py_object
429 )
430)]
431#[cfg_attr(
432 feature = "python",
433 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
434)]
435pub struct OneTickSlippageFillModel {
436 state: ProbabilisticFillState,
437}
438
439impl OneTickSlippageFillModel {
440 pub fn new(
446 prob_fill_on_limit: f64,
447 prob_slippage: f64,
448 random_seed: Option<u64>,
449 ) -> anyhow::Result<Self> {
450 Ok(Self {
451 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
452 })
453 }
454}
455
456impl Clone for OneTickSlippageFillModel {
457 fn clone(&self) -> Self {
458 Self {
459 state: self.state.clone(),
460 }
461 }
462}
463
464impl Default for OneTickSlippageFillModel {
465 fn default() -> Self {
466 Self::new(1.0, 0.0, None).unwrap()
467 }
468}
469
470impl FillModel for OneTickSlippageFillModel {
471 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
472 Ok(self.state.is_limit_filled())
473 }
474
475 fn is_slipped(&mut self) -> anyhow::Result<bool> {
476 Ok(self.state.is_slipped())
477 }
478
479 fn get_orderbook_for_fill_simulation(
480 &mut self,
481 instrument: &InstrumentAny,
482 _order: &OrderAny,
483 best_bid: Price,
484 best_ask: Price,
485 ) -> anyhow::Result<Option<OrderBook>> {
486 let tick = instrument.price_increment();
487 let size_prec = instrument.size_precision();
488 let mut book = build_l2_book(instrument.id());
489
490 add_order(
491 &mut book,
492 OrderSide::Buy,
493 best_bid - tick,
494 unlimited_liquidity(size_prec),
495 1,
496 );
497 add_order(
498 &mut book,
499 OrderSide::Sell,
500 best_ask + tick,
501 unlimited_liquidity(size_prec),
502 2,
503 );
504 Ok(Some(book))
505 }
506}
507
508#[derive(Debug)]
510#[cfg_attr(
511 feature = "python",
512 pyo3::pyclass(
513 module = "nautilus_trader.core.nautilus_pyo3.execution",
514 unsendable,
515 from_py_object
516 )
517)]
518#[cfg_attr(
519 feature = "python",
520 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
521)]
522pub struct ProbabilisticFillModel {
523 state: ProbabilisticFillState,
524}
525
526impl ProbabilisticFillModel {
527 pub fn new(
533 prob_fill_on_limit: f64,
534 prob_slippage: f64,
535 random_seed: Option<u64>,
536 ) -> anyhow::Result<Self> {
537 Ok(Self {
538 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
539 })
540 }
541}
542
543impl Clone for ProbabilisticFillModel {
544 fn clone(&self) -> Self {
545 Self {
546 state: self.state.clone(),
547 }
548 }
549}
550
551impl Default for ProbabilisticFillModel {
552 fn default() -> Self {
553 Self::new(1.0, 0.0, None).unwrap()
554 }
555}
556
557impl FillModel for ProbabilisticFillModel {
558 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
559 Ok(self.state.is_limit_filled())
560 }
561
562 fn is_slipped(&mut self) -> anyhow::Result<bool> {
563 Ok(self.state.is_slipped())
564 }
565
566 fn get_orderbook_for_fill_simulation(
567 &mut self,
568 instrument: &InstrumentAny,
569 _order: &OrderAny,
570 best_bid: Price,
571 best_ask: Price,
572 ) -> anyhow::Result<Option<OrderBook>> {
573 let tick = instrument.price_increment();
574 let size_prec = instrument.size_precision();
575 let mut book = build_l2_book(instrument.id());
576
577 if self.state.random_bool(0.5) {
578 add_order(
579 &mut book,
580 OrderSide::Buy,
581 best_bid,
582 unlimited_liquidity(size_prec),
583 1,
584 );
585 add_order(
586 &mut book,
587 OrderSide::Sell,
588 best_ask,
589 unlimited_liquidity(size_prec),
590 2,
591 );
592 } else {
593 add_order(
594 &mut book,
595 OrderSide::Buy,
596 best_bid - tick,
597 unlimited_liquidity(size_prec),
598 1,
599 );
600 add_order(
601 &mut book,
602 OrderSide::Sell,
603 best_ask + tick,
604 unlimited_liquidity(size_prec),
605 2,
606 );
607 }
608 Ok(Some(book))
609 }
610}
611
612#[derive(Debug)]
614#[cfg_attr(
615 feature = "python",
616 pyo3::pyclass(
617 module = "nautilus_trader.core.nautilus_pyo3.execution",
618 unsendable,
619 from_py_object
620 )
621)]
622#[cfg_attr(
623 feature = "python",
624 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
625)]
626pub struct TwoTierFillModel {
627 state: ProbabilisticFillState,
628}
629
630impl TwoTierFillModel {
631 pub fn new(
637 prob_fill_on_limit: f64,
638 prob_slippage: f64,
639 random_seed: Option<u64>,
640 ) -> anyhow::Result<Self> {
641 Ok(Self {
642 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
643 })
644 }
645}
646
647impl Clone for TwoTierFillModel {
648 fn clone(&self) -> Self {
649 Self {
650 state: self.state.clone(),
651 }
652 }
653}
654
655impl Default for TwoTierFillModel {
656 fn default() -> Self {
657 Self::new(1.0, 0.0, None).unwrap()
658 }
659}
660
661impl FillModel for TwoTierFillModel {
662 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
663 Ok(self.state.is_limit_filled())
664 }
665
666 fn is_slipped(&mut self) -> anyhow::Result<bool> {
667 Ok(self.state.is_slipped())
668 }
669
670 fn get_orderbook_for_fill_simulation(
671 &mut self,
672 instrument: &InstrumentAny,
673 _order: &OrderAny,
674 best_bid: Price,
675 best_ask: Price,
676 ) -> anyhow::Result<Option<OrderBook>> {
677 let tick = instrument.price_increment();
678 let size_prec = instrument.size_precision();
679 let mut book = build_l2_book(instrument.id());
680
681 add_order(
682 &mut book,
683 OrderSide::Buy,
684 best_bid,
685 Quantity::new(10.0, size_prec),
686 1,
687 );
688 add_order(
689 &mut book,
690 OrderSide::Sell,
691 best_ask,
692 Quantity::new(10.0, size_prec),
693 2,
694 );
695 add_order(
696 &mut book,
697 OrderSide::Buy,
698 best_bid - tick,
699 unlimited_liquidity(size_prec),
700 3,
701 );
702 add_order(
703 &mut book,
704 OrderSide::Sell,
705 best_ask + tick,
706 unlimited_liquidity(size_prec),
707 4,
708 );
709 Ok(Some(book))
710 }
711}
712
713#[derive(Debug)]
715#[cfg_attr(
716 feature = "python",
717 pyo3::pyclass(
718 module = "nautilus_trader.core.nautilus_pyo3.execution",
719 unsendable,
720 from_py_object
721 )
722)]
723#[cfg_attr(
724 feature = "python",
725 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
726)]
727pub struct ThreeTierFillModel {
728 state: ProbabilisticFillState,
729}
730
731impl ThreeTierFillModel {
732 pub fn new(
738 prob_fill_on_limit: f64,
739 prob_slippage: f64,
740 random_seed: Option<u64>,
741 ) -> anyhow::Result<Self> {
742 Ok(Self {
743 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
744 })
745 }
746}
747
748impl Clone for ThreeTierFillModel {
749 fn clone(&self) -> Self {
750 Self {
751 state: self.state.clone(),
752 }
753 }
754}
755
756impl Default for ThreeTierFillModel {
757 fn default() -> Self {
758 Self::new(1.0, 0.0, None).unwrap()
759 }
760}
761
762impl FillModel for ThreeTierFillModel {
763 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
764 Ok(self.state.is_limit_filled())
765 }
766
767 fn is_slipped(&mut self) -> anyhow::Result<bool> {
768 Ok(self.state.is_slipped())
769 }
770
771 fn get_orderbook_for_fill_simulation(
772 &mut self,
773 instrument: &InstrumentAny,
774 _order: &OrderAny,
775 best_bid: Price,
776 best_ask: Price,
777 ) -> anyhow::Result<Option<OrderBook>> {
778 let tick = instrument.price_increment();
779 let two_ticks = tick + tick;
780 let size_prec = instrument.size_precision();
781 let mut book = build_l2_book(instrument.id());
782
783 add_order(
784 &mut book,
785 OrderSide::Buy,
786 best_bid,
787 Quantity::new(50.0, size_prec),
788 1,
789 );
790 add_order(
791 &mut book,
792 OrderSide::Sell,
793 best_ask,
794 Quantity::new(50.0, size_prec),
795 2,
796 );
797 add_order(
798 &mut book,
799 OrderSide::Buy,
800 best_bid - tick,
801 Quantity::new(30.0, size_prec),
802 3,
803 );
804 add_order(
805 &mut book,
806 OrderSide::Sell,
807 best_ask + tick,
808 Quantity::new(30.0, size_prec),
809 4,
810 );
811 add_order(
812 &mut book,
813 OrderSide::Buy,
814 best_bid - two_ticks,
815 Quantity::new(20.0, size_prec),
816 5,
817 );
818 add_order(
819 &mut book,
820 OrderSide::Sell,
821 best_ask + two_ticks,
822 Quantity::new(20.0, size_prec),
823 6,
824 );
825 Ok(Some(book))
826 }
827}
828
829#[derive(Debug)]
831#[cfg_attr(
832 feature = "python",
833 pyo3::pyclass(
834 module = "nautilus_trader.core.nautilus_pyo3.execution",
835 unsendable,
836 from_py_object
837 )
838)]
839#[cfg_attr(
840 feature = "python",
841 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
842)]
843pub struct LimitOrderPartialFillModel {
844 state: ProbabilisticFillState,
845}
846
847impl LimitOrderPartialFillModel {
848 pub fn new(
854 prob_fill_on_limit: f64,
855 prob_slippage: f64,
856 random_seed: Option<u64>,
857 ) -> anyhow::Result<Self> {
858 Ok(Self {
859 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
860 })
861 }
862}
863
864impl Clone for LimitOrderPartialFillModel {
865 fn clone(&self) -> Self {
866 Self {
867 state: self.state.clone(),
868 }
869 }
870}
871
872impl Default for LimitOrderPartialFillModel {
873 fn default() -> Self {
874 Self::new(1.0, 0.0, None).unwrap()
875 }
876}
877
878impl FillModel for LimitOrderPartialFillModel {
879 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
880 Ok(self.state.is_limit_filled())
881 }
882
883 fn is_slipped(&mut self) -> anyhow::Result<bool> {
884 Ok(self.state.is_slipped())
885 }
886
887 fn get_orderbook_for_fill_simulation(
888 &mut self,
889 instrument: &InstrumentAny,
890 _order: &OrderAny,
891 best_bid: Price,
892 best_ask: Price,
893 ) -> anyhow::Result<Option<OrderBook>> {
894 let tick = instrument.price_increment();
895 let size_prec = instrument.size_precision();
896 let mut book = build_l2_book(instrument.id());
897
898 add_order(
899 &mut book,
900 OrderSide::Buy,
901 best_bid,
902 Quantity::new(5.0, size_prec),
903 1,
904 );
905 add_order(
906 &mut book,
907 OrderSide::Sell,
908 best_ask,
909 Quantity::new(5.0, size_prec),
910 2,
911 );
912 add_order(
913 &mut book,
914 OrderSide::Buy,
915 best_bid - tick,
916 unlimited_liquidity(size_prec),
917 3,
918 );
919 add_order(
920 &mut book,
921 OrderSide::Sell,
922 best_ask + tick,
923 unlimited_liquidity(size_prec),
924 4,
925 );
926 Ok(Some(book))
927 }
928}
929
930#[derive(Debug)]
933#[cfg_attr(
934 feature = "python",
935 pyo3::pyclass(
936 module = "nautilus_trader.core.nautilus_pyo3.execution",
937 unsendable,
938 from_py_object
939 )
940)]
941#[cfg_attr(
942 feature = "python",
943 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
944)]
945pub struct SizeAwareFillModel {
946 state: ProbabilisticFillState,
947}
948
949impl SizeAwareFillModel {
950 pub fn new(
956 prob_fill_on_limit: f64,
957 prob_slippage: f64,
958 random_seed: Option<u64>,
959 ) -> anyhow::Result<Self> {
960 Ok(Self {
961 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
962 })
963 }
964}
965
966impl Clone for SizeAwareFillModel {
967 fn clone(&self) -> Self {
968 Self {
969 state: self.state.clone(),
970 }
971 }
972}
973
974impl Default for SizeAwareFillModel {
975 fn default() -> Self {
976 Self::new(1.0, 0.0, None).unwrap()
977 }
978}
979
980impl FillModel for SizeAwareFillModel {
981 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
982 Ok(self.state.is_limit_filled())
983 }
984
985 fn is_slipped(&mut self) -> anyhow::Result<bool> {
986 Ok(self.state.is_slipped())
987 }
988
989 fn get_orderbook_for_fill_simulation(
990 &mut self,
991 instrument: &InstrumentAny,
992 order: &OrderAny,
993 best_bid: Price,
994 best_ask: Price,
995 ) -> anyhow::Result<Option<OrderBook>> {
996 let tick = instrument.price_increment();
997 let size_prec = instrument.size_precision();
998 let mut book = build_l2_book(instrument.id());
999
1000 let threshold = Quantity::new(10.0, size_prec);
1001 if order.quantity() <= threshold {
1002 add_order(
1004 &mut book,
1005 OrderSide::Buy,
1006 best_bid,
1007 Quantity::new(50.0, size_prec),
1008 1,
1009 );
1010 add_order(
1011 &mut book,
1012 OrderSide::Sell,
1013 best_ask,
1014 Quantity::new(50.0, size_prec),
1015 2,
1016 );
1017 } else {
1018 let remaining = order.quantity() - threshold;
1020 add_order(&mut book, OrderSide::Buy, best_bid, threshold, 1);
1021 add_order(&mut book, OrderSide::Sell, best_ask, threshold, 2);
1022 add_order(&mut book, OrderSide::Buy, best_bid - tick, remaining, 3);
1023 add_order(&mut book, OrderSide::Sell, best_ask + tick, remaining, 4);
1024 }
1025 Ok(Some(book))
1026 }
1027}
1028
1029#[derive(Debug)]
1031#[cfg_attr(
1032 feature = "python",
1033 pyo3::pyclass(
1034 module = "nautilus_trader.core.nautilus_pyo3.execution",
1035 unsendable,
1036 from_py_object
1037 )
1038)]
1039#[cfg_attr(
1040 feature = "python",
1041 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1042)]
1043pub struct CompetitionAwareFillModel {
1044 state: ProbabilisticFillState,
1045 liquidity_factor: Decimal,
1046}
1047
1048impl CompetitionAwareFillModel {
1049 pub fn new(
1055 prob_fill_on_limit: f64,
1056 prob_slippage: f64,
1057 random_seed: Option<u64>,
1058 liquidity_factor: f64,
1059 ) -> anyhow::Result<Self> {
1060 let state = ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?;
1061 check_in_range_inclusive_f64(liquidity_factor, 0.0, 1.0, "liquidity_factor")?;
1062 let liquidity_factor = Decimal::try_from(liquidity_factor)?;
1063
1064 Ok(Self {
1065 state,
1066 liquidity_factor,
1067 })
1068 }
1069}
1070
1071impl Clone for CompetitionAwareFillModel {
1072 fn clone(&self) -> Self {
1073 Self {
1074 state: self.state.clone(),
1075 liquidity_factor: self.liquidity_factor,
1076 }
1077 }
1078}
1079
1080impl Default for CompetitionAwareFillModel {
1081 fn default() -> Self {
1082 Self::new(1.0, 0.0, None, 0.3).unwrap()
1083 }
1084}
1085
1086impl FillModel for CompetitionAwareFillModel {
1087 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1088 Ok(self.state.is_limit_filled())
1089 }
1090
1091 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1092 Ok(self.state.is_slipped())
1093 }
1094
1095 fn get_orderbook_for_fill_simulation(
1096 &mut self,
1097 instrument: &InstrumentAny,
1098 _order: &OrderAny,
1099 best_bid: Price,
1100 best_ask: Price,
1101 ) -> anyhow::Result<Option<OrderBook>> {
1102 let size_prec = instrument.size_precision();
1103 let mut book = build_l2_book(instrument.id());
1104
1105 let available = Quantity::from_decimal_dp(
1107 (dec!(1000) * self.liquidity_factor).max(Decimal::ONE),
1108 size_prec,
1109 )?;
1110
1111 add_order(&mut book, OrderSide::Buy, best_bid, available, 1);
1112 add_order(&mut book, OrderSide::Sell, best_ask, available, 2);
1113 Ok(Some(book))
1114 }
1115}
1116
1117#[derive(Debug)]
1120#[cfg_attr(
1121 feature = "python",
1122 pyo3::pyclass(
1123 module = "nautilus_trader.core.nautilus_pyo3.execution",
1124 unsendable,
1125 from_py_object
1126 )
1127)]
1128#[cfg_attr(
1129 feature = "python",
1130 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1131)]
1132pub struct VolumeSensitiveFillModel {
1133 state: ProbabilisticFillState,
1134 recent_volume: f64,
1135}
1136
1137impl VolumeSensitiveFillModel {
1138 pub fn new(
1144 prob_fill_on_limit: f64,
1145 prob_slippage: f64,
1146 random_seed: Option<u64>,
1147 ) -> anyhow::Result<Self> {
1148 Ok(Self {
1149 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1150 recent_volume: 1000.0,
1151 })
1152 }
1153
1154 pub fn set_recent_volume(&mut self, volume: f64) {
1155 self.recent_volume = volume;
1156 }
1157}
1158
1159impl Clone for VolumeSensitiveFillModel {
1160 fn clone(&self) -> Self {
1161 Self {
1162 state: self.state.clone(),
1163 recent_volume: self.recent_volume,
1164 }
1165 }
1166}
1167
1168impl Default for VolumeSensitiveFillModel {
1169 fn default() -> Self {
1170 Self::new(1.0, 0.0, None).unwrap()
1171 }
1172}
1173
1174impl FillModel for VolumeSensitiveFillModel {
1175 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1176 Ok(self.state.is_limit_filled())
1177 }
1178
1179 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1180 Ok(self.state.is_slipped())
1181 }
1182
1183 fn get_orderbook_for_fill_simulation(
1184 &mut self,
1185 instrument: &InstrumentAny,
1186 _order: &OrderAny,
1187 best_bid: Price,
1188 best_ask: Price,
1189 ) -> anyhow::Result<Option<OrderBook>> {
1190 let tick = instrument.price_increment();
1191 let size_prec = instrument.size_precision();
1192 let mut book = build_l2_book(instrument.id());
1193
1194 check_non_negative_f64(self.recent_volume, "recent_volume")?;
1195 let recent_volume = Decimal::try_from(self.recent_volume)?;
1196
1197 let available =
1199 Quantity::from_decimal_dp((recent_volume * dec!(0.25)).max(Decimal::ONE), size_prec)?;
1200
1201 add_order(&mut book, OrderSide::Buy, best_bid, available, 1);
1202 add_order(&mut book, OrderSide::Sell, best_ask, available, 2);
1203 add_order(
1204 &mut book,
1205 OrderSide::Buy,
1206 best_bid - tick,
1207 unlimited_liquidity(size_prec),
1208 3,
1209 );
1210 add_order(
1211 &mut book,
1212 OrderSide::Sell,
1213 best_ask + tick,
1214 unlimited_liquidity(size_prec),
1215 4,
1216 );
1217 Ok(Some(book))
1218 }
1219}
1220
1221#[derive(Debug)]
1224#[cfg_attr(
1225 feature = "python",
1226 pyo3::pyclass(
1227 module = "nautilus_trader.core.nautilus_pyo3.execution",
1228 unsendable,
1229 from_py_object
1230 )
1231)]
1232#[cfg_attr(
1233 feature = "python",
1234 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
1235)]
1236pub struct MarketHoursFillModel {
1237 state: ProbabilisticFillState,
1238 is_low_liquidity: bool,
1239}
1240
1241impl MarketHoursFillModel {
1242 pub fn new(
1248 prob_fill_on_limit: f64,
1249 prob_slippage: f64,
1250 random_seed: Option<u64>,
1251 ) -> anyhow::Result<Self> {
1252 Ok(Self {
1253 state: ProbabilisticFillState::new(prob_fill_on_limit, prob_slippage, random_seed)?,
1254 is_low_liquidity: false,
1255 })
1256 }
1257
1258 pub fn set_low_liquidity_period(&mut self, is_low_liquidity: bool) {
1259 self.is_low_liquidity = is_low_liquidity;
1260 }
1261
1262 pub fn is_low_liquidity_period(&self) -> bool {
1263 self.is_low_liquidity
1264 }
1265}
1266
1267impl Clone for MarketHoursFillModel {
1268 fn clone(&self) -> Self {
1269 Self {
1270 state: self.state.clone(),
1271 is_low_liquidity: self.is_low_liquidity,
1272 }
1273 }
1274}
1275
1276impl Default for MarketHoursFillModel {
1277 fn default() -> Self {
1278 Self::new(1.0, 0.0, None).unwrap()
1279 }
1280}
1281
1282impl FillModel for MarketHoursFillModel {
1283 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1284 Ok(self.state.is_limit_filled())
1285 }
1286
1287 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1288 Ok(self.state.is_slipped())
1289 }
1290
1291 fn get_orderbook_for_fill_simulation(
1292 &mut self,
1293 instrument: &InstrumentAny,
1294 _order: &OrderAny,
1295 best_bid: Price,
1296 best_ask: Price,
1297 ) -> anyhow::Result<Option<OrderBook>> {
1298 let tick = instrument.price_increment();
1299 let size_prec = instrument.size_precision();
1300 let mut book = build_l2_book(instrument.id());
1301 let normal_volume = 500.0;
1302
1303 if self.is_low_liquidity {
1304 add_order(
1305 &mut book,
1306 OrderSide::Buy,
1307 best_bid - tick,
1308 Quantity::new(normal_volume, size_prec),
1309 1,
1310 );
1311 add_order(
1312 &mut book,
1313 OrderSide::Sell,
1314 best_ask + tick,
1315 Quantity::new(normal_volume, size_prec),
1316 2,
1317 );
1318 } else {
1319 add_order(
1320 &mut book,
1321 OrderSide::Buy,
1322 best_bid,
1323 Quantity::new(normal_volume, size_prec),
1324 1,
1325 );
1326 add_order(
1327 &mut book,
1328 OrderSide::Sell,
1329 best_ask,
1330 Quantity::new(normal_volume, size_prec),
1331 2,
1332 );
1333 }
1334 Ok(Some(book))
1335 }
1336}
1337
1338#[derive(Clone, Debug)]
1339pub enum FillModelAny {
1340 Default(DefaultFillModel),
1341 BestPrice(BestPriceFillModel),
1342 OneTickSlippage(OneTickSlippageFillModel),
1343 Probabilistic(ProbabilisticFillModel),
1344 TwoTier(TwoTierFillModel),
1345 ThreeTier(ThreeTierFillModel),
1346 LimitOrderPartialFill(LimitOrderPartialFillModel),
1347 SizeAware(SizeAwareFillModel),
1348 CompetitionAware(CompetitionAwareFillModel),
1349 VolumeSensitive(VolumeSensitiveFillModel),
1350 MarketHours(MarketHoursFillModel),
1351}
1352
1353impl FillModel for FillModelAny {
1354 fn is_limit_filled(&mut self) -> anyhow::Result<bool> {
1355 match self {
1356 Self::Default(m) => m.is_limit_filled(),
1357 Self::BestPrice(m) => m.is_limit_filled(),
1358 Self::OneTickSlippage(m) => m.is_limit_filled(),
1359 Self::Probabilistic(m) => m.is_limit_filled(),
1360 Self::TwoTier(m) => m.is_limit_filled(),
1361 Self::ThreeTier(m) => m.is_limit_filled(),
1362 Self::LimitOrderPartialFill(m) => m.is_limit_filled(),
1363 Self::SizeAware(m) => m.is_limit_filled(),
1364 Self::CompetitionAware(m) => m.is_limit_filled(),
1365 Self::VolumeSensitive(m) => m.is_limit_filled(),
1366 Self::MarketHours(m) => m.is_limit_filled(),
1367 }
1368 }
1369
1370 fn fill_limit_inside_spread(&self) -> anyhow::Result<bool> {
1371 match self {
1372 Self::Default(m) => m.fill_limit_inside_spread(),
1373 Self::BestPrice(m) => m.fill_limit_inside_spread(),
1374 Self::OneTickSlippage(m) => m.fill_limit_inside_spread(),
1375 Self::Probabilistic(m) => m.fill_limit_inside_spread(),
1376 Self::TwoTier(m) => m.fill_limit_inside_spread(),
1377 Self::ThreeTier(m) => m.fill_limit_inside_spread(),
1378 Self::LimitOrderPartialFill(m) => m.fill_limit_inside_spread(),
1379 Self::SizeAware(m) => m.fill_limit_inside_spread(),
1380 Self::CompetitionAware(m) => m.fill_limit_inside_spread(),
1381 Self::VolumeSensitive(m) => m.fill_limit_inside_spread(),
1382 Self::MarketHours(m) => m.fill_limit_inside_spread(),
1383 }
1384 }
1385
1386 fn is_slipped(&mut self) -> anyhow::Result<bool> {
1387 match self {
1388 Self::Default(m) => m.is_slipped(),
1389 Self::BestPrice(m) => m.is_slipped(),
1390 Self::OneTickSlippage(m) => m.is_slipped(),
1391 Self::Probabilistic(m) => m.is_slipped(),
1392 Self::TwoTier(m) => m.is_slipped(),
1393 Self::ThreeTier(m) => m.is_slipped(),
1394 Self::LimitOrderPartialFill(m) => m.is_slipped(),
1395 Self::SizeAware(m) => m.is_slipped(),
1396 Self::CompetitionAware(m) => m.is_slipped(),
1397 Self::VolumeSensitive(m) => m.is_slipped(),
1398 Self::MarketHours(m) => m.is_slipped(),
1399 }
1400 }
1401
1402 fn get_orderbook_for_fill_simulation(
1403 &mut self,
1404 instrument: &InstrumentAny,
1405 order: &OrderAny,
1406 best_bid: Price,
1407 best_ask: Price,
1408 ) -> anyhow::Result<Option<OrderBook>> {
1409 match self {
1410 Self::Default(m) => {
1411 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1412 }
1413 Self::BestPrice(m) => {
1414 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1415 }
1416 Self::OneTickSlippage(m) => {
1417 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1418 }
1419 Self::Probabilistic(m) => {
1420 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1421 }
1422 Self::TwoTier(m) => {
1423 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1424 }
1425 Self::ThreeTier(m) => {
1426 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1427 }
1428 Self::LimitOrderPartialFill(m) => {
1429 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1430 }
1431 Self::SizeAware(m) => {
1432 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1433 }
1434 Self::CompetitionAware(m) => {
1435 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1436 }
1437 Self::VolumeSensitive(m) => {
1438 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1439 }
1440 Self::MarketHours(m) => {
1441 m.get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask)
1442 }
1443 }
1444 }
1445}
1446
1447impl Default for FillModelAny {
1448 fn default() -> Self {
1449 Self::Default(DefaultFillModel::default())
1450 }
1451}
1452
1453impl Display for FillModelAny {
1454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1455 match self {
1456 Self::Default(m) => write!(f, "{m}"),
1457 Self::BestPrice(_) => write!(f, "BestPriceFillModel"),
1458 Self::OneTickSlippage(_) => write!(f, "OneTickSlippageFillModel"),
1459 Self::Probabilistic(_) => write!(f, "ProbabilisticFillModel"),
1460 Self::TwoTier(_) => write!(f, "TwoTierFillModel"),
1461 Self::ThreeTier(_) => write!(f, "ThreeTierFillModel"),
1462 Self::LimitOrderPartialFill(_) => write!(f, "LimitOrderPartialFillModel"),
1463 Self::SizeAware(_) => write!(f, "SizeAwareFillModel"),
1464 Self::CompetitionAware(_) => write!(f, "CompetitionAwareFillModel"),
1465 Self::VolumeSensitive(_) => write!(f, "VolumeSensitiveFillModel"),
1466 Self::MarketHours(_) => write!(f, "MarketHoursFillModel"),
1467 }
1468 }
1469}
1470
1471#[cfg(test)]
1472mod tests {
1473 use nautilus_core::correctness::CorrectnessError;
1474 use nautilus_model::{
1475 enums::OrderType,
1476 instruments::stubs::{audusd_sim, crypto_perpetual_ethusdt},
1477 orders::builder::OrderTestBuilder,
1478 };
1479 use rstest::{fixture, rstest};
1480
1481 use super::*;
1482
1483 #[fixture]
1484 fn fill_model() -> DefaultFillModel {
1485 let seed = 42;
1486 DefaultFillModel::new(0.5, 0.1, Some(seed)).unwrap()
1487 }
1488
1489 #[rstest]
1490 fn test_fill_model_param_prob_fill_on_limit_error() {
1491 let error = DefaultFillModel::new(1.1, 0.1, None).unwrap_err();
1492
1493 assert_eq!(
1494 error.downcast_ref::<CorrectnessError>(),
1495 Some(&CorrectnessError::OutOfRange {
1496 param: "prob_fill_on_limit".to_string(),
1497 min: "0".to_string(),
1498 max: "1".to_string(),
1499 value: "1.1".to_string(),
1500 type_name: "f64",
1501 })
1502 );
1503 assert_eq!(
1504 error.to_string(),
1505 "invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
1506 );
1507 }
1508
1509 #[rstest]
1510 fn test_fill_model_param_prob_slippage_error() {
1511 let error = DefaultFillModel::new(0.5, 1.1, None).unwrap_err();
1512
1513 assert_eq!(
1514 error.downcast_ref::<CorrectnessError>(),
1515 Some(&CorrectnessError::OutOfRange {
1516 param: "prob_slippage".to_string(),
1517 min: "0".to_string(),
1518 max: "1".to_string(),
1519 value: "1.1".to_string(),
1520 type_name: "f64",
1521 })
1522 );
1523 assert_eq!(
1524 error.to_string(),
1525 "invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
1526 );
1527 }
1528
1529 #[rstest]
1530 #[case(f64::NAN, "NaN")]
1531 #[case(f64::INFINITY, "inf")]
1532 #[case(f64::NEG_INFINITY, "-inf")]
1533 fn test_competition_aware_fill_model_rejects_non_finite_liquidity_factor(
1534 #[case] value: f64,
1535 #[case] expected_value: &str,
1536 ) {
1537 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1538
1539 assert_eq!(
1540 error.downcast_ref::<CorrectnessError>(),
1541 Some(&CorrectnessError::InvalidValue {
1542 param: "liquidity_factor".to_string(),
1543 value: expected_value.to_string(),
1544 type_name: "f64",
1545 })
1546 );
1547 }
1548
1549 #[rstest]
1550 #[case(-0.1, "-0.1")]
1551 #[case(1.1, "1.1")]
1552 fn test_competition_aware_fill_model_rejects_out_of_range_liquidity_factor(
1553 #[case] value: f64,
1554 #[case] expected_value: &str,
1555 ) {
1556 let error = CompetitionAwareFillModel::new(1.0, 0.0, None, value).unwrap_err();
1557
1558 assert_eq!(
1559 error.downcast_ref::<CorrectnessError>(),
1560 Some(&CorrectnessError::OutOfRange {
1561 param: "liquidity_factor".to_string(),
1562 min: "0".to_string(),
1563 max: "1".to_string(),
1564 value: expected_value.to_string(),
1565 type_name: "f64",
1566 })
1567 );
1568 }
1569
1570 #[rstest]
1571 #[case(f64::NAN, "NaN")]
1572 #[case(f64::INFINITY, "inf")]
1573 #[case(f64::NEG_INFINITY, "-inf")]
1574 fn test_volume_sensitive_fill_model_rejects_non_finite_volume(
1575 #[case] volume: f64,
1576 #[case] expected_value: &str,
1577 ) {
1578 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1579 let order = OrderTestBuilder::new(OrderType::Market)
1580 .instrument_id(instrument.id())
1581 .side(OrderSide::Buy)
1582 .quantity(Quantity::from(100_000))
1583 .build();
1584 let mut model = VolumeSensitiveFillModel::default();
1585 model.set_recent_volume(volume);
1586
1587 let error = model
1588 .get_orderbook_for_fill_simulation(
1589 &instrument,
1590 &order,
1591 Price::from("0.80000"),
1592 Price::from("0.80010"),
1593 )
1594 .unwrap_err();
1595
1596 assert_eq!(
1597 error.downcast_ref::<CorrectnessError>(),
1598 Some(&CorrectnessError::InvalidValue {
1599 param: "recent_volume".to_string(),
1600 value: expected_value.to_string(),
1601 type_name: "f64",
1602 })
1603 );
1604 }
1605
1606 #[rstest]
1607 fn test_volume_sensitive_fill_model_rejects_negative_volume() {
1608 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1609 let order = OrderTestBuilder::new(OrderType::Market)
1610 .instrument_id(instrument.id())
1611 .side(OrderSide::Buy)
1612 .quantity(Quantity::from(100_000))
1613 .build();
1614 let mut model = VolumeSensitiveFillModel::default();
1615 model.set_recent_volume(-1.0);
1616
1617 let error = model
1618 .get_orderbook_for_fill_simulation(
1619 &instrument,
1620 &order,
1621 Price::from("0.80000"),
1622 Price::from("0.80010"),
1623 )
1624 .unwrap_err();
1625
1626 assert_eq!(
1627 error.downcast_ref::<CorrectnessError>(),
1628 Some(&CorrectnessError::NegativeValue {
1629 param: "recent_volume".to_string(),
1630 value: "-1".to_string(),
1631 type_name: "f64",
1632 })
1633 );
1634 }
1635
1636 #[rstest]
1637 fn test_volume_sensitive_fill_model_rejects_volume_above_quantity_range() {
1638 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1639 let order = OrderTestBuilder::new(OrderType::Market)
1640 .instrument_id(instrument.id())
1641 .side(OrderSide::Buy)
1642 .quantity(Quantity::from(100_000))
1643 .build();
1644 let mut model = VolumeSensitiveFillModel::default();
1645 model.set_recent_volume(100_000_000_000_000_000.0);
1646
1647 let error = model
1648 .get_orderbook_for_fill_simulation(
1649 &instrument,
1650 &order,
1651 Price::from("0.80000"),
1652 Price::from("0.80010"),
1653 )
1654 .unwrap_err();
1655
1656 assert!(matches!(
1657 error.downcast_ref::<CorrectnessError>(),
1658 Some(CorrectnessError::PredicateViolation { message })
1659 if message.contains("QuantityRaw") || message.contains("QUANTITY_RAW_MAX")
1660 ));
1661 }
1662
1663 #[rstest]
1664 #[case(0.0, Quantity::from(1))]
1665 #[case(0.5, Quantity::from(500))]
1666 #[case(1.0, Quantity::from(1_000))]
1667 fn test_competition_aware_fill_model_builds_expected_liquidity(
1668 #[case] liquidity_factor: f64,
1669 #[case] expected_size: Quantity,
1670 ) {
1671 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1672 let order = OrderTestBuilder::new(OrderType::Market)
1673 .instrument_id(instrument.id())
1674 .side(OrderSide::Buy)
1675 .quantity(Quantity::from(100_000))
1676 .build();
1677 let best_bid = Price::from("0.80000");
1678 let best_ask = Price::from("0.80010");
1679 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, liquidity_factor).unwrap();
1680
1681 let book = model
1682 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1683 .unwrap()
1684 .unwrap();
1685
1686 assert_eq!(book.best_bid_price(), Some(best_bid));
1687 assert_eq!(book.best_ask_price(), Some(best_ask));
1688 assert_eq!(book.best_bid_size(), Some(expected_size));
1689 assert_eq!(book.best_ask_size(), Some(expected_size));
1690 }
1691
1692 #[rstest]
1693 fn test_competition_aware_fill_model_preserves_instrument_size_precision() {
1694 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1695 let order = OrderTestBuilder::new(OrderType::Market)
1696 .instrument_id(instrument.id())
1697 .side(OrderSide::Buy)
1698 .quantity(Quantity::from(100_000))
1699 .build();
1700 let best_bid = Price::from("2000.00");
1701 let best_ask = Price::from("2000.01");
1702 let mut model = CompetitionAwareFillModel::new(1.0, 0.0, None, 0.001234).unwrap();
1703
1704 let book = model
1705 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1706 .unwrap()
1707 .unwrap();
1708
1709 assert_eq!(book.best_bid_price(), Some(best_bid));
1710 assert_eq!(book.best_ask_price(), Some(best_ask));
1711 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.234")));
1712 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.234")));
1713 }
1714
1715 #[rstest]
1716 fn test_volume_sensitive_fill_model_builds_expected_liquidity() {
1717 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
1718 let order = OrderTestBuilder::new(OrderType::Market)
1719 .instrument_id(instrument.id())
1720 .side(OrderSide::Buy)
1721 .quantity(Quantity::from(100_000))
1722 .build();
1723 let best_bid = Price::from("2000.00");
1724 let best_ask = Price::from("2000.01");
1725 let mut model = VolumeSensitiveFillModel::default();
1726 model.set_recent_volume(5.678);
1727
1728 let book = model
1729 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1730 .unwrap()
1731 .unwrap();
1732
1733 assert_eq!(book.best_bid_price(), Some(best_bid));
1734 assert_eq!(book.best_ask_price(), Some(best_ask));
1735 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.420")));
1736 assert_eq!(book.best_ask_size(), Some(Quantity::from("1.420")));
1737 }
1738
1739 #[rstest]
1740 fn test_fill_model_is_limit_filled(mut fill_model: DefaultFillModel) {
1741 let result = fill_model.is_limit_filled().unwrap();
1743 assert!(!result);
1744 }
1745
1746 #[rstest]
1747 fn test_fill_model_is_slipped(mut fill_model: DefaultFillModel) {
1748 let result = fill_model.is_slipped().unwrap();
1750 assert!(!result);
1751 }
1752
1753 #[rstest]
1754 fn test_default_fill_model_returns_none() {
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
1762 let mut model = DefaultFillModel::default();
1763 let result = model
1764 .get_orderbook_for_fill_simulation(
1765 &instrument,
1766 &order,
1767 Price::from("0.80000"),
1768 Price::from("0.80010"),
1769 )
1770 .unwrap();
1771 assert!(result.is_none());
1772 }
1773
1774 #[rstest]
1775 fn test_best_price_fill_model_returns_book() {
1776 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1777 let order = OrderTestBuilder::new(OrderType::Market)
1778 .instrument_id(instrument.id())
1779 .side(OrderSide::Buy)
1780 .quantity(Quantity::from(100_000))
1781 .build();
1782
1783 let mut model = BestPriceFillModel::default();
1784 let result = model
1785 .get_orderbook_for_fill_simulation(
1786 &instrument,
1787 &order,
1788 Price::from("0.80000"),
1789 Price::from("0.80010"),
1790 )
1791 .unwrap();
1792 assert!(result.is_some());
1793 let book = result.unwrap();
1794 assert_eq!(book.best_bid_price().unwrap(), Price::from("0.80000"));
1795 assert_eq!(book.best_ask_price().unwrap(), Price::from("0.80010"));
1796 }
1797
1798 #[rstest]
1799 fn test_one_tick_slippage_fill_model() {
1800 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1801 let order = OrderTestBuilder::new(OrderType::Market)
1802 .instrument_id(instrument.id())
1803 .side(OrderSide::Buy)
1804 .quantity(Quantity::from(100_000))
1805 .build();
1806
1807 let tick = instrument.price_increment();
1808 let best_bid = Price::from("0.80000");
1809 let best_ask = Price::from("0.80010");
1810
1811 let mut model = OneTickSlippageFillModel::default();
1812 let result = model
1813 .get_orderbook_for_fill_simulation(&instrument, &order, best_bid, best_ask)
1814 .unwrap();
1815 assert!(result.is_some());
1816 let book = result.unwrap();
1817
1818 assert_eq!(book.best_bid_price().unwrap(), best_bid - tick);
1819 assert_eq!(book.best_ask_price().unwrap(), best_ask + tick);
1820 }
1821
1822 #[rstest]
1823 fn test_fill_model_any_dispatch() {
1824 let model = FillModelAny::default();
1825 assert!(matches!(model, FillModelAny::Default(_)));
1826 }
1827
1828 #[rstest]
1829 fn test_fill_model_any_is_limit_filled() {
1830 let mut model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.1, Some(42)).unwrap());
1831 let result = model.is_limit_filled().unwrap();
1832 assert!(!result);
1833 }
1834
1835 #[rstest]
1836 fn test_fill_model_handle_from_any_owns_state_per_conversion() {
1837 let model = FillModelAny::Default(DefaultFillModel::new(0.5, 0.0, Some(42)).unwrap());
1838 let mut expected_model = model.clone();
1839 let mut first: FillModelHandle = model.clone().into();
1840 let mut second: FillModelHandle = model.into();
1841
1842 let expected: Vec<_> = (0..16)
1843 .map(|_| expected_model.is_limit_filled().unwrap())
1844 .collect();
1845 let first_results: Vec<_> = (0..16).map(|_| first.is_limit_filled().unwrap()).collect();
1846 let second_results: Vec<_> = (0..16).map(|_| second.is_limit_filled().unwrap()).collect();
1847 let has_variation = expected.windows(2).any(|window| window[0] != window[1]);
1848
1849 assert!(has_variation);
1850 assert_eq!(first_results, expected);
1851 assert_eq!(second_results, expected);
1852 }
1853
1854 #[rstest]
1855 fn test_default_fill_model_fill_limit_inside_spread_is_false() {
1856 let model = DefaultFillModel::default();
1857 assert!(!model.fill_limit_inside_spread().unwrap());
1858 }
1859
1860 #[rstest]
1861 fn test_best_price_fill_model_fill_limit_inside_spread_is_true() {
1862 let model = BestPriceFillModel::default();
1863 assert!(model.fill_limit_inside_spread().unwrap());
1864 }
1865
1866 #[rstest]
1867 fn test_one_tick_slippage_fill_model_fill_limit_inside_spread_is_false() {
1868 let model = OneTickSlippageFillModel::default();
1869 assert!(!model.fill_limit_inside_spread().unwrap());
1870 }
1871
1872 #[rstest]
1873 fn test_fill_model_any_fill_limit_inside_spread_dispatch() {
1874 let default = FillModelAny::Default(DefaultFillModel::default());
1875 assert!(!default.fill_limit_inside_spread().unwrap());
1876
1877 let best_price = FillModelAny::BestPrice(BestPriceFillModel::default());
1878 assert!(best_price.fill_limit_inside_spread().unwrap());
1879
1880 let one_tick = FillModelAny::OneTickSlippage(OneTickSlippageFillModel::default());
1881 assert!(!one_tick.fill_limit_inside_spread().unwrap());
1882 }
1883}