nautilus_execution/models/
fill.rs

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