Skip to main content

nautilus_execution/models/
latency.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::{
17    fmt::{Debug, Display},
18    rc::Rc,
19};
20
21use nautilus_core::DurationNanos;
22
23/// Trait for latency models used in backtesting.
24///
25/// Latency models simulate network delays for order operations during backtesting.
26/// Implementations can provide static or dynamic (jittered) latency values.
27pub trait LatencyModel: Debug {
28    /// Returns the latency for order insertion operations.
29    fn get_insert_latency(&self) -> DurationNanos;
30
31    /// Returns the latency for order update/modify operations.
32    fn get_update_latency(&self) -> DurationNanos;
33
34    /// Returns the latency for order delete/cancel operations.
35    fn get_delete_latency(&self) -> DurationNanos;
36
37    /// Returns the base latency component.
38    fn get_base_latency(&self) -> DurationNanos;
39}
40
41/// Shared runtime handle for a latency model.
42#[derive(Clone)]
43pub struct LatencyModelHandle(Rc<dyn LatencyModel>);
44
45impl LatencyModelHandle {
46    /// Creates a new [`LatencyModelHandle`] from a latency model.
47    #[must_use]
48    pub fn new<T>(model: T) -> Self
49    where
50        T: LatencyModel + 'static,
51    {
52        Self(Rc::new(model))
53    }
54
55    /// Creates a new [`LatencyModelHandle`] from an existing reference-counted model.
56    #[must_use]
57    pub fn from_rc(model: Rc<dyn LatencyModel>) -> Self {
58        Self(model)
59    }
60}
61
62impl Debug for LatencyModelHandle {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_tuple(stringify!(LatencyModelHandle))
65            .field(&"<dyn LatencyModel>")
66            .finish()
67    }
68}
69
70impl LatencyModel for LatencyModelHandle {
71    fn get_insert_latency(&self) -> DurationNanos {
72        self.0.get_insert_latency()
73    }
74
75    fn get_update_latency(&self) -> DurationNanos {
76        self.0.get_update_latency()
77    }
78
79    fn get_delete_latency(&self) -> DurationNanos {
80        self.0.get_delete_latency()
81    }
82
83    fn get_base_latency(&self) -> DurationNanos {
84        self.0.get_base_latency()
85    }
86}
87
88#[derive(Debug, Clone)]
89pub enum LatencyModelAny {
90    Static(StaticLatencyModel),
91}
92
93impl LatencyModel for LatencyModelAny {
94    fn get_insert_latency(&self) -> DurationNanos {
95        match self {
96            Self::Static(model) => model.get_insert_latency(),
97        }
98    }
99
100    fn get_update_latency(&self) -> DurationNanos {
101        match self {
102            Self::Static(model) => model.get_update_latency(),
103        }
104    }
105
106    fn get_delete_latency(&self) -> DurationNanos {
107        match self {
108            Self::Static(model) => model.get_delete_latency(),
109        }
110    }
111
112    fn get_base_latency(&self) -> DurationNanos {
113        match self {
114            Self::Static(model) => model.get_base_latency(),
115        }
116    }
117}
118
119impl From<LatencyModelAny> for LatencyModelHandle {
120    fn from(model: LatencyModelAny) -> Self {
121        Self::new(model)
122    }
123}
124
125/// Static latency model with fixed latency values.
126///
127/// Models the latency for different order operations including base network latency
128/// and specific operation latencies for insert, update, and delete operations.
129///
130/// The base latency is automatically added to each operation latency, matching
131/// Python's behavior. For example, if `base_latency_nanos = 100ms` and
132/// `insert_latency_nanos = 200ms`, the effective insert latency will be 300ms.
133#[derive(Debug, Clone)]
134#[cfg_attr(
135    feature = "python",
136    pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
137)]
138#[cfg_attr(
139    feature = "python",
140    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
141)]
142#[allow(
143    clippy::struct_field_names,
144    reason = "latency_nanos suffix consistently identifies latency types"
145)]
146pub struct StaticLatencyModel {
147    base_latency_nanos: DurationNanos,
148    insert_latency_nanos: DurationNanos,
149    update_latency_nanos: DurationNanos,
150    delete_latency_nanos: DurationNanos,
151}
152
153impl StaticLatencyModel {
154    /// Creates a new [`StaticLatencyModel`] instance.
155    ///
156    /// The base latency is added to each operation latency to get the effective latency.
157    ///
158    /// # Arguments
159    ///
160    /// * `base_latency_nanos` - Base network latency added to all operations
161    /// * `insert_latency_nanos` - Additional latency for order insertion
162    /// * `update_latency_nanos` - Additional latency for order updates
163    /// * `delete_latency_nanos` - Additional latency for order cancellation
164    #[must_use]
165    pub fn new(
166        base_latency_nanos: DurationNanos,
167        insert_latency_nanos: DurationNanos,
168        update_latency_nanos: DurationNanos,
169        delete_latency_nanos: DurationNanos,
170    ) -> Self {
171        Self {
172            base_latency_nanos,
173            insert_latency_nanos: base_latency_nanos + insert_latency_nanos,
174            update_latency_nanos: base_latency_nanos + update_latency_nanos,
175            delete_latency_nanos: base_latency_nanos + delete_latency_nanos,
176        }
177    }
178}
179
180impl LatencyModel for StaticLatencyModel {
181    fn get_insert_latency(&self) -> DurationNanos {
182        self.insert_latency_nanos
183    }
184
185    fn get_update_latency(&self) -> DurationNanos {
186        self.update_latency_nanos
187    }
188
189    fn get_delete_latency(&self) -> DurationNanos {
190        self.delete_latency_nanos
191    }
192
193    fn get_base_latency(&self) -> DurationNanos {
194        self.base_latency_nanos
195    }
196}
197
198impl Display for StaticLatencyModel {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(f, "LatencyModel()")
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use rstest::rstest;
207
208    use super::*;
209
210    #[derive(Debug)]
211    struct CustomLatencyModel;
212
213    impl LatencyModel for CustomLatencyModel {
214        fn get_insert_latency(&self) -> DurationNanos {
215            DurationNanos::new(11)
216        }
217
218        fn get_update_latency(&self) -> DurationNanos {
219            DurationNanos::new(22)
220        }
221
222        fn get_delete_latency(&self) -> DurationNanos {
223            DurationNanos::new(33)
224        }
225
226        fn get_base_latency(&self) -> DurationNanos {
227            DurationNanos::new(44)
228        }
229    }
230
231    #[rstest]
232    fn test_latency_model_handle_calls_custom_model() {
233        let model: Rc<dyn LatencyModel> = Rc::new(CustomLatencyModel);
234        let handle = LatencyModelHandle::from_rc(model);
235        let cloned_handle = handle.clone();
236        drop(handle);
237
238        assert_eq!(cloned_handle.get_insert_latency(), DurationNanos::new(11));
239        assert_eq!(cloned_handle.get_update_latency(), DurationNanos::new(22));
240        assert_eq!(cloned_handle.get_delete_latency(), DurationNanos::new(33));
241        assert_eq!(cloned_handle.get_base_latency(), DurationNanos::new(44));
242    }
243
244    #[rstest]
245    fn test_latency_model_handle_from_any_preserves_model() {
246        let model = StaticLatencyModel::new(
247            DurationNanos::new(1),
248            DurationNanos::new(10),
249            DurationNanos::new(20),
250            DurationNanos::new(30),
251        );
252        let handle: LatencyModelHandle = LatencyModelAny::Static(model).into();
253
254        assert_eq!(handle.get_insert_latency(), DurationNanos::new(11));
255        assert_eq!(handle.get_update_latency(), DurationNanos::new(21));
256        assert_eq!(handle.get_delete_latency(), DurationNanos::new(31));
257        assert_eq!(handle.get_base_latency(), DurationNanos::new(1));
258    }
259
260    #[rstest]
261    fn test_static_latency_model() {
262        let model = StaticLatencyModel::new(
263            DurationNanos::from_millis(1),
264            DurationNanos::from_millis(2),
265            DurationNanos::from_millis(3),
266            DurationNanos::from_millis(4),
267        );
268
269        // Base is added to each operation latency
270        assert_eq!(model.get_insert_latency().as_u64(), 3_000_000);
271        assert_eq!(model.get_update_latency().as_u64(), 4_000_000);
272        assert_eq!(model.get_delete_latency().as_u64(), 5_000_000);
273        assert_eq!(model.get_base_latency().as_u64(), 1_000_000);
274    }
275}