nautilus_backtest/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::{check_in_range_inclusive_f64, FAILED};
19use rand::{rngs::StdRng, Rng, SeedableRng};
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    pub fn new(
36        prob_fill_on_limit: f64,
37        prob_fill_on_stop: f64,
38        prob_slippage: f64,
39        random_seed: Option<u64>,
40    ) -> anyhow::Result<Self> {
41        check_in_range_inclusive_f64(prob_fill_on_limit, 0.0, 1.0, "prob_fill_on_limit")
42            .expect(FAILED);
43        check_in_range_inclusive_f64(prob_fill_on_stop, 0.0, 1.0, "prob_fill_on_stop")
44            .expect(FAILED);
45        check_in_range_inclusive_f64(prob_slippage, 0.0, 1.0, "prob_slippage").expect(FAILED);
46        let rng = match random_seed {
47            Some(seed) => StdRng::seed_from_u64(seed),
48            None => StdRng::from_entropy(),
49        };
50        Ok(Self {
51            prob_fill_on_limit,
52            prob_fill_on_stop,
53            prob_slippage,
54            rng,
55        })
56    }
57
58    pub fn is_limit_filled(&mut self) -> bool {
59        self.event_success(self.prob_fill_on_limit)
60    }
61
62    pub fn is_stop_filled(&mut self) -> bool {
63        self.event_success(self.prob_fill_on_stop)
64    }
65
66    pub fn is_slipped(&mut self) -> bool {
67        self.event_success(self.prob_slippage)
68    }
69
70    fn event_success(&mut self, probability: f64) -> bool {
71        match probability {
72            0.0 => false,
73            1.0 => true,
74            _ => self.rng.gen_bool(probability),
75        }
76    }
77}
78
79impl Display for FillModel {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(
82            f,
83            "FillModel(prob_fill_on_limit: {}, prob_fill_on_stop: {}, prob_slippage: {})",
84            self.prob_fill_on_limit, self.prob_fill_on_stop, self.prob_slippage
85        )
86    }
87}
88
89impl Default for FillModel {
90    /// Creates a new default [`FillModel`] instance.
91    fn default() -> Self {
92        Self::new(0.5, 0.5, 0.1, None).unwrap()
93    }
94}
95
96////////////////////////////////////////////////////////////////////////////////
97// Tests
98////////////////////////////////////////////////////////////////////////////////
99#[cfg(test)]
100mod tests {
101    use rstest::{fixture, rstest};
102
103    use super::*;
104
105    #[fixture]
106    fn fill_model() -> FillModel {
107        let seed = 42;
108        FillModel::new(0.5, 0.5, 0.1, Some(seed)).unwrap()
109    }
110
111    #[rstest]
112    #[should_panic(
113        expected = "Condition failed: invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
114    )]
115    fn test_fill_model_param_prob_fill_on_limit_error() {
116        let _ = super::FillModel::new(1.1, 0.5, 0.1, None).unwrap();
117    }
118
119    #[rstest]
120    #[should_panic(
121        expected = "Condition failed: invalid f64 for 'prob_fill_on_stop' not in range [0, 1], was 1.1"
122    )]
123    fn test_fill_model_param_prob_fill_on_stop_error() {
124        let _ = super::FillModel::new(0.5, 1.1, 0.1, None).unwrap();
125    }
126
127    #[rstest]
128    #[should_panic(
129        expected = "Condition failed: invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
130    )]
131    fn test_fill_model_param_prob_slippage_error() {
132        let _ = super::FillModel::new(0.5, 0.5, 1.1, None).unwrap();
133    }
134
135    #[rstest]
136    fn test_fill_model_is_limit_filled(mut fill_model: FillModel) {
137        // because of fixed seed this is deterministic
138        let result = fill_model.is_limit_filled();
139        assert!(!result);
140    }
141
142    #[rstest]
143    fn test_fill_model_is_stop_filled(mut fill_model: FillModel) {
144        // because of fixed seed this is deterministic
145        let result = fill_model.is_stop_filled();
146        assert!(!result);
147    }
148
149    #[rstest]
150    fn test_fill_model_is_slipped(mut fill_model: FillModel) {
151        // because of fixed seed this is deterministic
152        let result = fill_model.is_slipped();
153        assert!(!result);
154    }
155}