Skip to main content

nautilus_execution/models/
fill.rs

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