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