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