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::correctness::{FAILED, check_in_range_inclusive_f64};
19use rand::{Rng, SeedableRng, rngs::StdRng};
20
21#[derive(Debug, Clone)]
22pub struct FillModel {
23    /// The probability of limit order filling if the market rests on its price.
24    prob_fill_on_limit: f64,
25    /// The probability of order fill prices slipping by one tick.
26    prob_slippage: f64,
27    /// Random number generator
28    rng: StdRng,
29}
30
31impl FillModel {
32    /// Creates a new [`FillModel`] instance.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if any probability parameter is out of range [0.0, 1.0].
37    ///
38    /// # Panics
39    ///
40    /// Panics if probability checks fail.
41    pub fn new(
42        prob_fill_on_limit: f64,
43        prob_slippage: f64,
44        random_seed: Option<u64>,
45    ) -> anyhow::Result<Self> {
46        check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")
47            .expect(FAILED);
48        check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage").expect(FAILED);
49        let rng = match random_seed {
50            Some(seed) => StdRng::seed_from_u64(seed),
51            None => StdRng::from_os_rng(),
52        };
53        Ok(Self {
54            prob_fill_on_limit,
55            prob_slippage,
56            rng,
57        })
58    }
59
60    /// Returns `true` if a limit order should be filled based on the configured probability.
61    pub fn is_limit_filled(&mut self) -> bool {
62        self.event_success(self.prob_fill_on_limit)
63    }
64
65    /// Returns `true` if an order should slip by one tick based on the configured probability.
66    pub fn is_slipped(&mut self) -> bool {
67        self.event_success(self.prob_slippage)
68    }
69
70    /// Returns a simulated `OrderBook` for fill simulation.
71    ///
72    /// This method allows custom fill models to provide their own liquidity
73    /// simulation by returning a custom `OrderBook` that represents the expected
74    /// market liquidity. The matching engine will use this simulated `OrderBook`
75    /// to determine fills.
76    ///
77    /// The default implementation returns None, which means the matching engine
78    /// will use its standard fill logic (maintaining backward compatibility).
79    pub fn get_orderbook_for_fill_simulation(
80        &self,
81        _instrument: &dyn std::any::Any, // Placeholder for instrument type
82        _order: &dyn std::any::Any,      // Placeholder for order type
83        _best_bid: f64,
84        _best_ask: f64,
85    ) -> Option<Box<dyn std::any::Any>> {
86        None // Default implementation - use existing fill logic
87    }
88
89    fn event_success(&mut self, probability: f64) -> bool {
90        match probability {
91            0.0 => false,
92            1.0 => true,
93            _ => self.rng.random_bool(probability),
94        }
95    }
96}
97
98impl Display for FillModel {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        write!(
101            f,
102            "FillModel(prob_fill_on_limit: {}, prob_slippage: {})",
103            self.prob_fill_on_limit, self.prob_slippage
104        )
105    }
106}
107
108impl Default for FillModel {
109    /// Creates a new default [`FillModel`] instance.
110    fn default() -> Self {
111        Self::new(1.0, 0.0, None).unwrap()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use rstest::{fixture, rstest};
118
119    use super::*;
120
121    #[fixture]
122    fn fill_model() -> FillModel {
123        let seed = 42;
124        FillModel::new(0.5, 0.1, Some(seed)).unwrap()
125    }
126
127    #[rstest]
128    #[should_panic(
129        expected = "Condition failed: invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
130    )]
131    fn test_fill_model_param_prob_fill_on_limit_error() {
132        let _ = super::FillModel::new(1.1, 0.1, None).unwrap();
133    }
134
135    #[rstest]
136    #[should_panic(
137        expected = "Condition failed: invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
138    )]
139    fn test_fill_model_param_prob_slippage_error() {
140        let _ = super::FillModel::new(0.5, 1.1, None).unwrap();
141    }
142
143    #[rstest]
144    fn test_fill_model_is_limit_filled(mut fill_model: FillModel) {
145        // Fixed seed makes this deterministic
146        let result = fill_model.is_limit_filled();
147        assert!(!result);
148    }
149
150    #[rstest]
151    fn test_fill_model_is_slipped(mut fill_model: FillModel) {
152        // Fixed seed makes this deterministic
153        let result = fill_model.is_slipped();
154        assert!(!result);
155    }
156}