nautilus_execution/models/
latency.rs1use 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
27pub trait LatencyModel: Debug {
32 fn get_insert_latency(
41 &self,
42 command: &TradingCommand,
43 ts_now: UnixNanos,
44 ) -> anyhow::Result<DurationNanos>;
45
46 fn get_update_latency(
55 &self,
56 command: &TradingCommand,
57 ts_now: UnixNanos,
58 ) -> anyhow::Result<DurationNanos>;
59
60 fn get_delete_latency(
69 &self,
70 command: &TradingCommand,
71 ts_now: UnixNanos,
72 ) -> anyhow::Result<DurationNanos>;
73
74 fn get_base_latency(&self) -> DurationNanos;
76}
77
78#[derive(Clone)]
80pub struct LatencyModelHandle(Rc<dyn LatencyModel>);
81
82impl LatencyModelHandle {
83 #[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 #[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#[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 #[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 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}