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