rusty_backtest/
latency.rs

1//! Simple Latency Models - Clean and accurate latency simulation
2//!
3//! Provides simple but realistic latency models for backtesting without
4//! excessive complexity. All latencies are in nanoseconds for precision.
5
6use parking_lot::Mutex;
7use rand::SeedableRng;
8use rand::rngs::StdRng;
9use rusty_common::collections::FxHashMap;
10use std::sync::Arc;
11
12/// Trait for latency models
13pub trait LatencyModel: Send + Sync {
14    /// Get the next latency value in nanoseconds
15    fn get_latency_ns(&self) -> u64;
16
17    /// Get the average latency
18    fn avg_latency_ns(&self) -> u64;
19
20    /// Clone the latency model
21    fn clone_box(&self) -> Box<dyn LatencyModel>;
22}
23
24/// Fixed latency model - Always returns the same value
25#[derive(Debug, Clone)]
26pub struct FixedLatency {
27    latency_ns: u64,
28}
29
30impl FixedLatency {
31    /// Create a new fixed latency model with latency in nanoseconds
32    #[must_use]
33    pub const fn new(latency_ns: u64) -> Self {
34        Self { latency_ns }
35    }
36
37    /// Create a fixed latency model from microseconds
38    #[must_use]
39    pub const fn from_micros(latency_us: u64) -> Self {
40        Self {
41            latency_ns: latency_us * 1000,
42        }
43    }
44
45    /// Create a fixed latency model from milliseconds
46    #[must_use]
47    pub const fn from_millis(latency_ms: u64) -> Self {
48        Self {
49            latency_ns: latency_ms * 1_000_000,
50        }
51    }
52}
53
54impl LatencyModel for FixedLatency {
55    fn get_latency_ns(&self) -> u64 {
56        self.latency_ns
57    }
58
59    fn avg_latency_ns(&self) -> u64 {
60        self.latency_ns
61    }
62
63    fn clone_box(&self) -> Box<dyn LatencyModel> {
64        Box::new(self.clone())
65    }
66}
67
68/// Gaussian (normal distribution) latency model
69pub struct GaussianLatency {
70    mean_ns: u64,
71    std_dev_ns: u64,
72    rng: Arc<Mutex<StdRng>>,
73}
74
75impl GaussianLatency {
76    /// Create a new Gaussian latency model with mean and standard deviation in nanoseconds
77    #[must_use]
78    pub fn new(mean_ns: u64, std_dev_ns: u64) -> Self {
79        Self {
80            mean_ns,
81            std_dev_ns,
82            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
83        }
84    }
85
86    /// Create a Gaussian latency model from microseconds
87    #[must_use]
88    pub fn from_micros(mean_us: u64, std_dev_us: u64) -> Self {
89        Self::new(mean_us * 1000, std_dev_us * 1000)
90    }
91
92    /// Create a Gaussian latency model with a specific seed for reproducibility
93    #[must_use]
94    pub fn with_seed(mean_ns: u64, std_dev_ns: u64, seed: u64) -> Self {
95        Self {
96            mean_ns,
97            std_dev_ns,
98            rng: Arc::new(Mutex::new(StdRng::seed_from_u64(seed))),
99        }
100    }
101
102    /// Box-Muller transform for normal distribution
103    fn sample_normal(&self, rng: &mut StdRng) -> f64 {
104        use rand::Rng;
105        use std::f64::consts::PI;
106
107        let u1: f64 = rng.random();
108        let u2: f64 = rng.random();
109
110        let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
111        self.mean_ns as f64 + z0 * self.std_dev_ns as f64
112    }
113}
114
115impl LatencyModel for GaussianLatency {
116    fn get_latency_ns(&self) -> u64 {
117        let mut rng = self.rng.lock();
118        let sample = self.sample_normal(&mut rng);
119        // Ensure non-negative latency
120        sample.max(0.0) as u64
121    }
122
123    fn avg_latency_ns(&self) -> u64 {
124        self.mean_ns
125    }
126
127    fn clone_box(&self) -> Box<dyn LatencyModel> {
128        Box::new(GaussianLatency {
129            mean_ns: self.mean_ns,
130            std_dev_ns: self.std_dev_ns,
131            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
132        })
133    }
134}
135
136/// Uniform random latency model
137pub struct UniformLatency {
138    min_ns: u64,
139    max_ns: u64,
140    rng: Arc<Mutex<StdRng>>,
141}
142
143impl UniformLatency {
144    /// Create a new uniform latency model with min and max in nanoseconds
145    #[must_use]
146    pub fn new(min_ns: u64, max_ns: u64) -> Self {
147        Self {
148            min_ns,
149            max_ns,
150            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
151        }
152    }
153
154    /// Create a uniform latency model from microseconds
155    #[must_use]
156    pub fn from_micros(min_us: u64, max_us: u64) -> Self {
157        Self::new(min_us * 1000, max_us * 1000)
158    }
159
160    /// Create a uniform latency model with a specific seed for reproducibility
161    #[must_use]
162    pub fn with_seed(min_ns: u64, max_ns: u64, seed: u64) -> Self {
163        Self {
164            min_ns,
165            max_ns,
166            rng: Arc::new(Mutex::new(StdRng::seed_from_u64(seed))),
167        }
168    }
169}
170
171impl LatencyModel for UniformLatency {
172    fn get_latency_ns(&self) -> u64 {
173        use rand::Rng;
174        let mut rng = self.rng.lock();
175        rng.random_range(self.min_ns..=self.max_ns)
176    }
177
178    fn avg_latency_ns(&self) -> u64 {
179        (self.min_ns + self.max_ns) / 2
180    }
181
182    fn clone_box(&self) -> Box<dyn LatencyModel> {
183        Box::new(UniformLatency {
184            min_ns: self.min_ns,
185            max_ns: self.max_ns,
186            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
187        })
188    }
189}
190
191/// Bimodal latency model - Models fast path and slow path
192pub struct BimodalLatency {
193    fast_latency: Box<dyn LatencyModel>,
194    slow_latency: Box<dyn LatencyModel>,
195    fast_probability: f64,
196    rng: Arc<Mutex<StdRng>>,
197}
198
199impl BimodalLatency {
200    /// Create a new bimodal latency model with fast and slow paths
201    #[must_use]
202    pub fn new(
203        fast_latency: Box<dyn LatencyModel>,
204        slow_latency: Box<dyn LatencyModel>,
205        fast_probability: f64,
206    ) -> Self {
207        Self {
208            fast_latency,
209            slow_latency,
210            fast_probability,
211            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
212        }
213    }
214
215    /// Create a bimodal latency model with a specific seed for reproducibility
216    pub fn with_seed(
217        fast_latency: Box<dyn LatencyModel>,
218        slow_latency: Box<dyn LatencyModel>,
219        fast_probability: f64,
220        seed: u64,
221    ) -> Self {
222        Self {
223            fast_latency,
224            slow_latency,
225            fast_probability,
226            rng: Arc::new(Mutex::new(StdRng::seed_from_u64(seed))),
227        }
228    }
229}
230
231impl LatencyModel for BimodalLatency {
232    fn get_latency_ns(&self) -> u64 {
233        use rand::Rng;
234        let mut rng = self.rng.lock();
235        if rng.random::<f64>() < self.fast_probability {
236            self.fast_latency.get_latency_ns()
237        } else {
238            self.slow_latency.get_latency_ns()
239        }
240    }
241
242    fn avg_latency_ns(&self) -> u64 {
243        let fast_avg = self.fast_latency.avg_latency_ns() as f64;
244        let slow_avg = self.slow_latency.avg_latency_ns() as f64;
245        (fast_avg * self.fast_probability + slow_avg * (1.0 - self.fast_probability)) as u64
246    }
247
248    fn clone_box(&self) -> Box<dyn LatencyModel> {
249        Box::new(BimodalLatency {
250            fast_latency: self.fast_latency.clone_box(),
251            slow_latency: self.slow_latency.clone_box(),
252            fast_probability: self.fast_probability,
253            rng: Arc::new(Mutex::new(StdRng::from_seed([0; 32]))),
254        })
255    }
256}
257
258/// Asset-specific latency model that can vary by symbol
259pub struct AssetLatency {
260    default_model: Box<dyn LatencyModel>,
261    asset_models: Arc<Mutex<FxHashMap<String, Box<dyn LatencyModel>>>>,
262}
263
264impl AssetLatency {
265    /// Create a new asset-specific latency model with a default fallback
266    #[must_use]
267    pub fn new(default_model: Box<dyn LatencyModel>) -> Self {
268        Self {
269            default_model,
270            asset_models: Arc::new(Mutex::new(FxHashMap::default())),
271        }
272    }
273
274    /// Add a symbol-specific latency model
275    pub fn add_asset(&self, symbol: String, model: Box<dyn LatencyModel>) {
276        self.asset_models.lock().insert(symbol, model);
277    }
278    /// Get latency for a specific asset, falling back to default if not found
279    pub fn get_latency_for_asset(&self, symbol: &str) -> u64 {
280        let models = self.asset_models.lock();
281        if let Some(model) = models.get(symbol) {
282            model.get_latency_ns()
283        } else {
284            self.default_model.get_latency_ns()
285        }
286    }
287}
288
289impl LatencyModel for AssetLatency {
290    fn get_latency_ns(&self) -> u64 {
291        self.default_model.get_latency_ns()
292    }
293
294    fn avg_latency_ns(&self) -> u64 {
295        self.default_model.avg_latency_ns()
296    }
297
298    fn clone_box(&self) -> Box<dyn LatencyModel> {
299        let new_models = Arc::new(Mutex::new(FxHashMap::default()));
300        let models = self.asset_models.lock();
301        for (symbol, model) in models.iter() {
302            new_models.lock().insert(symbol.clone(), model.clone_box());
303        }
304
305        Box::new(AssetLatency {
306            default_model: self.default_model.clone_box(),
307            asset_models: new_models,
308        })
309    }
310}
311
312/// Common latency presets for different network conditions
313pub mod presets {
314    use super::*;
315
316    /// Local/Colocation latency (< 100μs)
317    #[must_use]
318    pub fn colocation() -> Box<dyn LatencyModel> {
319        Box::new(GaussianLatency::new(50_000, 10_000)) // 50μs ± 10μs
320    }
321
322    /// Same datacenter latency (100-500μs)
323    #[must_use]
324    pub fn datacenter() -> Box<dyn LatencyModel> {
325        Box::new(GaussianLatency::new(250_000, 50_000)) // 250μs ± 50μs
326    }
327
328    /// Cross-region latency (1-10ms)
329    #[must_use]
330    pub fn cross_region() -> Box<dyn LatencyModel> {
331        Box::new(GaussianLatency::new(5_000_000, 1_000_000)) // 5ms ± 1ms
332    }
333
334    /// Internet latency (10-100ms)
335    #[must_use]
336    pub fn internet() -> Box<dyn LatencyModel> {
337        Box::new(BimodalLatency::new(
338            Box::new(GaussianLatency::new(20_000_000, 5_000_000)), // Fast: 20ms ± 5ms
339            Box::new(GaussianLatency::new(80_000_000, 20_000_000)), // Slow: 80ms ± 20ms
340            0.8,                                                   // 80% fast path
341        ))
342    }
343
344    /// Realistic exchange latency with jitter
345    #[must_use]
346    pub fn exchange_realistic() -> Box<dyn LatencyModel> {
347        Box::new(BimodalLatency::new(
348            Box::new(UniformLatency::new(100_000, 500_000)), // Fast: 100-500μs
349            Box::new(GaussianLatency::new(2_000_000, 500_000)), // Slow: 2ms ± 0.5ms
350            0.95,                                            // 95% fast path
351        ))
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_fixed_latency() {
361        let model = FixedLatency::from_micros(100);
362        assert_eq!(model.get_latency_ns(), 100_000);
363        assert_eq!(model.avg_latency_ns(), 100_000);
364    }
365
366    #[test]
367    fn test_gaussian_latency() {
368        let model = GaussianLatency::with_seed(1_000_000, 100_000, 42);
369        let samples: Vec<u64> = (0..1000).map(|_| model.get_latency_ns()).collect();
370
371        // Check that values are reasonable
372        let avg: u64 = samples.iter().sum::<u64>() / samples.len() as u64;
373        assert!(avg > 800_000 && avg < 1_200_000); // Within 20% of mean
374    }
375
376    #[test]
377    fn test_bimodal_latency() {
378        let fast = Box::new(FixedLatency::new(100_000));
379        let slow = Box::new(FixedLatency::new(1_000_000));
380        let model = BimodalLatency::with_seed(fast, slow, 0.8, 42);
381
382        let samples: Vec<u64> = (0..1000).map(|_| model.get_latency_ns()).collect();
383        let fast_count = samples.iter().filter(|&&x| x == 100_000).count();
384        let slow_count = samples.iter().filter(|&&x| x == 1_000_000).count();
385
386        assert_eq!(fast_count + slow_count, 1000);
387        // Should be roughly 80% fast
388        assert!(fast_count > 700 && fast_count < 900);
389    }
390}