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