Skip to main content

nautilus_execution/models/
fill.rs

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