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::UnixNanos;
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) -> UnixNanos;
30
31    /// Returns the latency for order update/modify operations.
32    fn get_update_latency(&self) -> UnixNanos;
33
34    /// Returns the latency for order delete/cancel operations.
35    fn get_delete_latency(&self) -> UnixNanos;
36
37    /// Returns the base latency component.
38    fn get_base_latency(&self) -> UnixNanos;
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) -> UnixNanos {
72        self.0.get_insert_latency()
73    }
74
75    fn get_update_latency(&self) -> UnixNanos {
76        self.0.get_update_latency()
77    }
78
79    fn get_delete_latency(&self) -> UnixNanos {
80        self.0.get_delete_latency()
81    }
82
83    fn get_base_latency(&self) -> UnixNanos {
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) -> UnixNanos {
95        match self {
96            Self::Static(model) => model.get_insert_latency(),
97        }
98    }
99
100    fn get_update_latency(&self) -> UnixNanos {
101        match self {
102            Self::Static(model) => model.get_update_latency(),
103        }
104    }
105
106    fn get_delete_latency(&self) -> UnixNanos {
107        match self {
108            Self::Static(model) => model.get_delete_latency(),
109        }
110    }
111
112    fn get_base_latency(&self) -> UnixNanos {
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: UnixNanos,
148    insert_latency_nanos: UnixNanos,
149    update_latency_nanos: UnixNanos,
150    delete_latency_nanos: UnixNanos,
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: UnixNanos,
167        insert_latency_nanos: UnixNanos,
168        update_latency_nanos: UnixNanos,
169        delete_latency_nanos: UnixNanos,
170    ) -> Self {
171        Self {
172            base_latency_nanos,
173            insert_latency_nanos: UnixNanos::from(
174                base_latency_nanos.as_u64() + insert_latency_nanos.as_u64(),
175            ),
176            update_latency_nanos: UnixNanos::from(
177                base_latency_nanos.as_u64() + update_latency_nanos.as_u64(),
178            ),
179            delete_latency_nanos: UnixNanos::from(
180                base_latency_nanos.as_u64() + delete_latency_nanos.as_u64(),
181            ),
182        }
183    }
184}
185
186impl LatencyModel for StaticLatencyModel {
187    fn get_insert_latency(&self) -> UnixNanos {
188        self.insert_latency_nanos
189    }
190
191    fn get_update_latency(&self) -> UnixNanos {
192        self.update_latency_nanos
193    }
194
195    fn get_delete_latency(&self) -> UnixNanos {
196        self.delete_latency_nanos
197    }
198
199    fn get_base_latency(&self) -> UnixNanos {
200        self.base_latency_nanos
201    }
202}
203
204impl Display for StaticLatencyModel {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        write!(f, "LatencyModel()")
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use rstest::rstest;
213
214    use super::*;
215
216    #[derive(Debug)]
217    struct CustomLatencyModel;
218
219    impl LatencyModel for CustomLatencyModel {
220        fn get_insert_latency(&self) -> UnixNanos {
221            UnixNanos::from(11)
222        }
223
224        fn get_update_latency(&self) -> UnixNanos {
225            UnixNanos::from(22)
226        }
227
228        fn get_delete_latency(&self) -> UnixNanos {
229            UnixNanos::from(33)
230        }
231
232        fn get_base_latency(&self) -> UnixNanos {
233            UnixNanos::from(44)
234        }
235    }
236
237    #[rstest]
238    fn test_latency_model_handle_calls_custom_model() {
239        let model: Rc<dyn LatencyModel> = Rc::new(CustomLatencyModel);
240        let handle = LatencyModelHandle::from_rc(model);
241        let cloned_handle = handle.clone();
242        drop(handle);
243
244        assert_eq!(cloned_handle.get_insert_latency(), UnixNanos::from(11));
245        assert_eq!(cloned_handle.get_update_latency(), UnixNanos::from(22));
246        assert_eq!(cloned_handle.get_delete_latency(), UnixNanos::from(33));
247        assert_eq!(cloned_handle.get_base_latency(), UnixNanos::from(44));
248    }
249
250    #[rstest]
251    fn test_latency_model_handle_from_any_preserves_model() {
252        let model = StaticLatencyModel::new(
253            UnixNanos::from(1),
254            UnixNanos::from(10),
255            UnixNanos::from(20),
256            UnixNanos::from(30),
257        );
258        let handle: LatencyModelHandle = LatencyModelAny::Static(model).into();
259
260        assert_eq!(handle.get_insert_latency(), UnixNanos::from(11));
261        assert_eq!(handle.get_update_latency(), UnixNanos::from(21));
262        assert_eq!(handle.get_delete_latency(), UnixNanos::from(31));
263        assert_eq!(handle.get_base_latency(), UnixNanos::from(1));
264    }
265
266    #[rstest]
267    fn test_static_latency_model() {
268        let model = StaticLatencyModel::new(
269            UnixNanos::from(1_000_000),
270            UnixNanos::from(2_000_000),
271            UnixNanos::from(3_000_000),
272            UnixNanos::from(4_000_000),
273        );
274
275        // Base is added to each operation latency
276        assert_eq!(model.get_insert_latency().as_u64(), 3_000_000);
277        assert_eq!(model.get_update_latency().as_u64(), 4_000_000);
278        assert_eq!(model.get_delete_latency().as_u64(), 5_000_000);
279        assert_eq!(model.get_base_latency().as_u64(), 1_000_000);
280    }
281}