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}