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// -------------------------------------------------------------------------------------------------
1516use std::fmt::Display;
1718use nautilus_core::correctness::{FAILED, check_in_range_inclusive_f64};
19use rand::{Rng, SeedableRng, rngs::StdRng};
2021#[derive(Debug, Clone)]
22pub struct FillModel {
23/// The probability of limit order filling if the market rests on its price.
24prob_fill_on_limit: f64,
25/// The probability of stop orders filling if the market rests on its price.
26prob_fill_on_stop: f64,
27/// The probability of order fill prices slipping by one tick.
28prob_slippage: f64,
29/// Random number generator
30rng: StdRng,
31}
3233impl FillModel {
34/// Creates a new [`FillModel`] instance.
35pub 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);
46let rng = match random_seed {
47Some(seed) => StdRng::seed_from_u64(seed),
48None => StdRng::from_os_rng(),
49 };
50Ok(Self {
51 prob_fill_on_limit,
52 prob_fill_on_stop,
53 prob_slippage,
54 rng,
55 })
56 }
5758pub fn is_limit_filled(&mut self) -> bool {
59self.event_success(self.prob_fill_on_limit)
60 }
6162pub fn is_stop_filled(&mut self) -> bool {
63self.event_success(self.prob_fill_on_stop)
64 }
6566pub fn is_slipped(&mut self) -> bool {
67self.event_success(self.prob_slippage)
68 }
6970fn event_success(&mut self, probability: f64) -> bool {
71match probability {
720.0 => false,
731.0 => true,
74_ => self.rng.random_bool(probability),
75 }
76 }
77}
7879impl Display for FillModel {
80fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81write!(
82 f,
83"FillModel(prob_fill_on_limit: {}, prob_fill_on_stop: {}, prob_slippage: {})",
84self.prob_fill_on_limit, self.prob_fill_on_stop, self.prob_slippage
85 )
86 }
87}
8889impl Default for FillModel {
90/// Creates a new default [`FillModel`] instance.
91fn default() -> Self {
92Self::new(0.5, 0.5, 0.1, None).unwrap()
93 }
94}
9596////////////////////////////////////////////////////////////////////////////////
97// Tests
98////////////////////////////////////////////////////////////////////////////////
99#[cfg(test)]
100mod tests {
101use rstest::{fixture, rstest};
102103use super::*;
104105#[fixture]
106fn fill_model() -> FillModel {
107let seed = 42;
108 FillModel::new(0.5, 0.5, 0.1, Some(seed)).unwrap()
109 }
110111#[rstest]
112 #[should_panic(
113 expected = "Condition failed: invalid f64 for 'prob_fill_on_limit' not in range [0, 1], was 1.1"
114)]
115fn test_fill_model_param_prob_fill_on_limit_error() {
116let _ = super::FillModel::new(1.1, 0.5, 0.1, None).unwrap();
117 }
118119#[rstest]
120 #[should_panic(
121 expected = "Condition failed: invalid f64 for 'prob_fill_on_stop' not in range [0, 1], was 1.1"
122)]
123fn test_fill_model_param_prob_fill_on_stop_error() {
124let _ = super::FillModel::new(0.5, 1.1, 0.1, None).unwrap();
125 }
126127#[rstest]
128 #[should_panic(
129 expected = "Condition failed: invalid f64 for 'prob_slippage' not in range [0, 1], was 1.1"
130)]
131fn test_fill_model_param_prob_slippage_error() {
132let _ = super::FillModel::new(0.5, 0.5, 1.1, None).unwrap();
133 }
134135#[rstest]
136fn test_fill_model_is_limit_filled(mut fill_model: FillModel) {
137// because of fixed seed this is deterministic
138let result = fill_model.is_limit_filled();
139assert!(!result);
140 }
141142#[rstest]
143fn test_fill_model_is_stop_filled(mut fill_model: FillModel) {
144// because of fixed seed this is deterministic
145let result = fill_model.is_stop_filled();
146assert!(!result);
147 }
148149#[rstest]
150fn test_fill_model_is_slipped(mut fill_model: FillModel) {
151// because of fixed seed this is deterministic
152let result = fill_model.is_slipped();
153assert!(!result);
154 }
155}