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_common::messages::execution::TradingCommand;
22use nautilus_core::{DurationNanos, UnixNanos};
23
24#[cfg(feature = "python")]
25use crate::python::latency::PythonLatencyModel;
26
27/// Trait for latency models used in backtesting.
28///
29/// Latency models simulate network delays for order operations during backtesting.
30/// Implementations can provide static or dynamic (jittered) latency values.
31pub trait LatencyModel: Debug {
32    /// Returns the latency for order insertion operations.
33    ///
34    /// `ts_now` is the current clock time (simulation time in backtests).
35    /// The command retains its creation timestamp.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the model cannot produce a valid delay.
40    fn get_insert_latency(
41        &self,
42        command: &TradingCommand,
43        ts_now: UnixNanos,
44    ) -> anyhow::Result<DurationNanos>;
45
46    /// Returns the latency for order update/modify operations.
47    ///
48    /// `ts_now` is the current clock time (simulation time in backtests).
49    /// The command retains its creation timestamp.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the model cannot produce a valid delay.
54    fn get_update_latency(
55        &self,
56        command: &TradingCommand,
57        ts_now: UnixNanos,
58    ) -> anyhow::Result<DurationNanos>;
59
60    /// Returns the latency for order delete/cancel operations.
61    ///
62    /// `ts_now` is the current clock time (simulation time in backtests).
63    /// The command retains its creation timestamp.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the model cannot produce a valid delay.
68    fn get_delete_latency(
69        &self,
70        command: &TradingCommand,
71        ts_now: UnixNanos,
72    ) -> anyhow::Result<DurationNanos>;
73
74    /// Returns the base latency component.
75    fn get_base_latency(&self) -> DurationNanos;
76}
77
78/// Shared runtime handle for a latency model.
79#[derive(Clone)]
80pub struct LatencyModelHandle(Rc<dyn LatencyModel>);
81
82impl LatencyModelHandle {
83    /// Creates a new [`LatencyModelHandle`] from a latency model.
84    #[must_use]
85    pub fn new<T>(model: T) -> Self
86    where
87        T: LatencyModel + 'static,
88    {
89        Self(Rc::new(model))
90    }
91
92    /// Creates a new [`LatencyModelHandle`] from an existing reference-counted model.
93    #[must_use]
94    pub fn from_rc(model: Rc<dyn LatencyModel>) -> Self {
95        Self(model)
96    }
97}
98
99impl Debug for LatencyModelHandle {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_tuple(stringify!(LatencyModelHandle))
102            .field(&"<dyn LatencyModel>")
103            .finish()
104    }
105}
106
107impl LatencyModel for LatencyModelHandle {
108    fn get_insert_latency(
109        &self,
110        command: &TradingCommand,
111        ts_now: UnixNanos,
112    ) -> anyhow::Result<DurationNanos> {
113        self.0.get_insert_latency(command, ts_now)
114    }
115
116    fn get_update_latency(
117        &self,
118        command: &TradingCommand,
119        ts_now: UnixNanos,
120    ) -> anyhow::Result<DurationNanos> {
121        self.0.get_update_latency(command, ts_now)
122    }
123
124    fn get_delete_latency(
125        &self,
126        command: &TradingCommand,
127        ts_now: UnixNanos,
128    ) -> anyhow::Result<DurationNanos> {
129        self.0.get_delete_latency(command, ts_now)
130    }
131
132    fn get_base_latency(&self) -> DurationNanos {
133        self.0.get_base_latency()
134    }
135}
136
137#[derive(Debug, Clone)]
138pub enum LatencyModelAny {
139    Static(StaticLatencyModel),
140    #[cfg(feature = "python")]
141    Python(PythonLatencyModel),
142}
143
144impl LatencyModel for LatencyModelAny {
145    fn get_insert_latency(
146        &self,
147        command: &TradingCommand,
148        ts_now: UnixNanos,
149    ) -> anyhow::Result<DurationNanos> {
150        match self {
151            Self::Static(model) => model.get_insert_latency(command, ts_now),
152            #[cfg(feature = "python")]
153            Self::Python(model) => model.get_insert_latency(command, ts_now),
154        }
155    }
156
157    fn get_update_latency(
158        &self,
159        command: &TradingCommand,
160        ts_now: UnixNanos,
161    ) -> anyhow::Result<DurationNanos> {
162        match self {
163            Self::Static(model) => model.get_update_latency(command, ts_now),
164            #[cfg(feature = "python")]
165            Self::Python(model) => model.get_update_latency(command, ts_now),
166        }
167    }
168
169    fn get_delete_latency(
170        &self,
171        command: &TradingCommand,
172        ts_now: UnixNanos,
173    ) -> anyhow::Result<DurationNanos> {
174        match self {
175            Self::Static(model) => model.get_delete_latency(command, ts_now),
176            #[cfg(feature = "python")]
177            Self::Python(model) => model.get_delete_latency(command, ts_now),
178        }
179    }
180
181    fn get_base_latency(&self) -> DurationNanos {
182        match self {
183            Self::Static(model) => model.get_base_latency(),
184            #[cfg(feature = "python")]
185            Self::Python(model) => model.get_base_latency(),
186        }
187    }
188}
189
190impl From<LatencyModelAny> for LatencyModelHandle {
191    fn from(model: LatencyModelAny) -> Self {
192        Self::new(model)
193    }
194}
195
196/// Static latency model with fixed latency values.
197///
198/// Models the latency for different order operations including base network latency
199/// and specific operation latencies for insert, update, and delete operations.
200///
201/// The base latency is automatically added to each operation latency, matching
202/// Python's behavior. For example, if `base_latency_nanos = 100ms` and
203/// `insert_latency_nanos = 200ms`, the effective insert latency will be 300ms.
204#[derive(Debug, Clone)]
205#[cfg_attr(
206    feature = "python",
207    pyo3::pyclass(module = "nautilus_trader.execution", unsendable, from_py_object)
208)]
209#[cfg_attr(
210    feature = "python",
211    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
212)]
213#[allow(
214    clippy::struct_field_names,
215    reason = "latency_nanos suffix consistently identifies latency types"
216)]
217pub struct StaticLatencyModel {
218    base_latency_nanos: DurationNanos,
219    insert_latency_nanos: DurationNanos,
220    update_latency_nanos: DurationNanos,
221    delete_latency_nanos: DurationNanos,
222}
223
224impl StaticLatencyModel {
225    /// Creates a new [`StaticLatencyModel`] instance.
226    ///
227    /// The base latency is added to each operation latency to get the effective latency.
228    ///
229    /// # Arguments
230    ///
231    /// * `base_latency_nanos` - Base network latency added to all operations
232    /// * `insert_latency_nanos` - Additional latency for order insertion
233    /// * `update_latency_nanos` - Additional latency for order updates
234    /// * `delete_latency_nanos` - Additional latency for order cancellation
235    #[must_use]
236    pub fn new(
237        base_latency_nanos: DurationNanos,
238        insert_latency_nanos: DurationNanos,
239        update_latency_nanos: DurationNanos,
240        delete_latency_nanos: DurationNanos,
241    ) -> Self {
242        Self {
243            base_latency_nanos,
244            insert_latency_nanos: base_latency_nanos + insert_latency_nanos,
245            update_latency_nanos: base_latency_nanos + update_latency_nanos,
246            delete_latency_nanos: base_latency_nanos + delete_latency_nanos,
247        }
248    }
249}
250
251impl LatencyModel for StaticLatencyModel {
252    fn get_insert_latency(
253        &self,
254        _command: &TradingCommand,
255        _ts_now: UnixNanos,
256    ) -> anyhow::Result<DurationNanos> {
257        Ok(self.insert_latency_nanos)
258    }
259
260    fn get_update_latency(
261        &self,
262        _command: &TradingCommand,
263        _ts_now: UnixNanos,
264    ) -> anyhow::Result<DurationNanos> {
265        Ok(self.update_latency_nanos)
266    }
267
268    fn get_delete_latency(
269        &self,
270        _command: &TradingCommand,
271        _ts_now: UnixNanos,
272    ) -> anyhow::Result<DurationNanos> {
273        Ok(self.delete_latency_nanos)
274    }
275
276    fn get_base_latency(&self) -> DurationNanos {
277        self.base_latency_nanos
278    }
279}
280
281impl Display for StaticLatencyModel {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "LatencyModel()")
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use nautilus_common::messages::execution::CancelAllOrders;
290    use nautilus_core::UUID4;
291    use nautilus_model::identifiers::{InstrumentId, StrategyId, TraderId};
292    use rstest::rstest;
293
294    use super::*;
295
296    fn latency_command() -> TradingCommand {
297        TradingCommand::CancelAllOrders(CancelAllOrders::new(
298            TraderId::from("TEST-001"),
299            None,
300            StrategyId::from("S-001"),
301            InstrumentId::from("AUD/USD.SIM"),
302            None,
303            UUID4::new(),
304            UnixNanos::default(),
305            None,
306            None,
307        ))
308    }
309
310    #[derive(Debug)]
311    struct CustomLatencyModel;
312
313    impl LatencyModel for CustomLatencyModel {
314        fn get_insert_latency(
315            &self,
316            _command: &TradingCommand,
317            _ts_now: UnixNanos,
318        ) -> anyhow::Result<DurationNanos> {
319            Ok(DurationNanos::new(11))
320        }
321
322        fn get_update_latency(
323            &self,
324            _command: &TradingCommand,
325            _ts_now: UnixNanos,
326        ) -> anyhow::Result<DurationNanos> {
327            Ok(DurationNanos::new(22))
328        }
329
330        fn get_delete_latency(
331            &self,
332            _command: &TradingCommand,
333            _ts_now: UnixNanos,
334        ) -> anyhow::Result<DurationNanos> {
335            Ok(DurationNanos::new(33))
336        }
337
338        fn get_base_latency(&self) -> DurationNanos {
339            DurationNanos::new(44)
340        }
341    }
342
343    #[rstest]
344    fn test_latency_model_handle_calls_custom_model() {
345        let command = latency_command();
346        let model: Rc<dyn LatencyModel> = Rc::new(CustomLatencyModel);
347        let handle = LatencyModelHandle::from_rc(model);
348        let cloned_handle = handle.clone();
349        drop(handle);
350
351        assert_eq!(
352            cloned_handle
353                .get_insert_latency(&command, UnixNanos::default())
354                .unwrap(),
355            DurationNanos::new(11)
356        );
357        assert_eq!(
358            cloned_handle
359                .get_update_latency(&command, UnixNanos::default())
360                .unwrap(),
361            DurationNanos::new(22)
362        );
363        assert_eq!(
364            cloned_handle
365                .get_delete_latency(&command, UnixNanos::default())
366                .unwrap(),
367            DurationNanos::new(33)
368        );
369        assert_eq!(cloned_handle.get_base_latency(), DurationNanos::new(44));
370    }
371
372    #[rstest]
373    fn test_latency_model_handle_from_any_preserves_model() {
374        let command = latency_command();
375
376        let model = StaticLatencyModel::new(
377            DurationNanos::new(1),
378            DurationNanos::new(10),
379            DurationNanos::new(20),
380            DurationNanos::new(30),
381        );
382        let handle: LatencyModelHandle = LatencyModelAny::Static(model).into();
383
384        assert_eq!(
385            handle
386                .get_insert_latency(&command, UnixNanos::default())
387                .unwrap(),
388            DurationNanos::new(11)
389        );
390        assert_eq!(
391            handle
392                .get_update_latency(&command, UnixNanos::default())
393                .unwrap(),
394            DurationNanos::new(21)
395        );
396        assert_eq!(
397            handle
398                .get_delete_latency(&command, UnixNanos::default())
399                .unwrap(),
400            DurationNanos::new(31)
401        );
402        assert_eq!(handle.get_base_latency(), DurationNanos::new(1));
403    }
404
405    #[rstest]
406    fn test_static_latency_model() {
407        let command = latency_command();
408
409        let model = StaticLatencyModel::new(
410            DurationNanos::from_millis(1),
411            DurationNanos::from_millis(2),
412            DurationNanos::from_millis(3),
413            DurationNanos::from_millis(4),
414        );
415
416        // Base is added to each operation latency
417        assert_eq!(
418            model
419                .get_insert_latency(&command, UnixNanos::default())
420                .unwrap()
421                .as_u64(),
422            3_000_000
423        );
424        assert_eq!(
425            model
426                .get_update_latency(&command, UnixNanos::default())
427                .unwrap()
428                .as_u64(),
429            4_000_000
430        );
431        assert_eq!(
432            model
433                .get_delete_latency(&command, UnixNanos::default())
434                .unwrap()
435                .as_u64(),
436            5_000_000
437        );
438        assert_eq!(model.get_base_latency().as_u64(), 1_000_000);
439    }
440}