rusty_common/
utils.rs

1//! Common utility functions
2
3use crate::SmartString;
4use rust_decimal::Decimal;
5use std::str::FromStr;
6// Re-export f32x8 for external users of simd_nan_safe functions
7#[allow(unused_imports)]
8use wide::f32x8;
9
10/// Parse decimal value from string, returning zero on error
11#[must_use]
12pub fn parse_decimal_safe(s: &str) -> Decimal {
13    Decimal::from_str(s).unwrap_or(Decimal::ZERO)
14}
15
16/// Parse optional decimal value from string
17#[must_use]
18pub fn parse_decimal_option(s: Option<&str>) -> Option<Decimal> {
19    s.and_then(|val| Decimal::from_str(val).ok())
20}
21
22/// Format decimal for API submission (removing trailing zeros)
23#[must_use]
24pub fn format_decimal(value: Decimal) -> SmartString {
25    let s = value.to_string();
26    if s.contains('.') {
27        s.trim_end_matches('0').trim_end_matches('.').into()
28    } else {
29        s.into()
30    }
31}
32
33/// Generate a unique order ID
34#[must_use]
35pub fn generate_order_id() -> SmartString {
36    uuid::Uuid::new_v4().to_string().into()
37}
38
39/// Normalize trading symbol (e.g., "BTC-USDT" -> "BTCUSDT")
40#[must_use]
41pub fn normalize_symbol(symbol: &str) -> SmartString {
42    symbol.replace(['-', '_', '/'], "").to_uppercase().into()
43}
44
45/// Parse symbol into base and quote currencies
46#[must_use]
47pub fn parse_symbol_pair(symbol: &str) -> Option<(SmartString, SmartString)> {
48    // Try different separators
49    if let Some(pos) = symbol.find('-') {
50        let (base, quote) = symbol.split_at(pos);
51        return Some((base.into(), quote[1..].into()));
52    }
53
54    if let Some(pos) = symbol.find('_') {
55        let (base, quote) = symbol.split_at(pos);
56        return Some((base.into(), quote[1..].into()));
57    }
58
59    if let Some(pos) = symbol.find('/') {
60        let (base, quote) = symbol.split_at(pos);
61        return Some((base.into(), quote[1..].into()));
62    }
63
64    // Try to parse common patterns (e.g., BTCUSDT)
65    if let Some(base) = symbol.strip_suffix("USDT") {
66        return Some((base.into(), "USDT".into()));
67    }
68
69    if let Some(base) = symbol.strip_suffix("BTC") {
70        return Some((base.into(), "BTC".into()));
71    }
72
73    if let Some(base) = symbol.strip_suffix("ETH") {
74        return Some((base.into(), "ETH".into()));
75    }
76
77    None
78}
79
80/// Convert basis points to decimal (1 bp = 0.01%)
81#[must_use]
82pub fn bp_to_decimal(bp: i32) -> Decimal {
83    Decimal::from(bp) / Decimal::from(10000)
84}
85
86/// Convert decimal to basis points
87#[must_use]
88pub fn decimal_to_bp(value: Decimal) -> i32 {
89    use rust_decimal::prelude::ToPrimitive;
90    (value * Decimal::from(10000)).round().to_i32().unwrap_or(0)
91}
92
93/// SmartString ID generation helpers to reduce duplication across exchange adapters
94pub mod id_generation {
95    use crate::SmartString;
96    use uuid::Uuid;
97
98    /// Generate an execution report ID with a prefix and suffix
99    #[inline]
100    #[must_use]
101    pub fn generate_report_id(prefix: &str, suffix: &str) -> SmartString {
102        let mut id = SmartString::new();
103        id.push_str(prefix);
104        id.push('_');
105        id.push_str(suffix);
106        id
107    }
108
109    /// Generate an execution report ID with just a prefix and a new UUID
110    #[inline]
111    #[must_use]
112    pub fn generate_report_id_with_uuid(prefix: &str) -> SmartString {
113        let uuid_str = Uuid::new_v4().to_string();
114        generate_report_id(prefix, &uuid_str)
115    }
116
117    /// Generate a multi-part ID with a separator
118    #[inline]
119    #[must_use]
120    pub fn generate_multi_part_id(parts: &[&str], separator: char) -> SmartString {
121        let mut id = SmartString::new();
122
123        for (i, part) in parts.iter().enumerate() {
124            if i > 0 {
125                id.push(separator);
126            }
127            id.push_str(part);
128        }
129        id
130    }
131
132    /// Generate a standard execution report ID for acknowledgments
133    #[inline]
134    #[must_use]
135    pub fn generate_ack_id(order_id: &str) -> SmartString {
136        generate_report_id("ack", order_id)
137    }
138
139    /// Generate a standard execution report ID for rejections
140    #[inline]
141    #[must_use]
142    pub fn generate_rejection_id(order_id: &str) -> SmartString {
143        generate_report_id("rej", order_id)
144    }
145
146    /// Generate a standard execution report ID for cancellations
147    #[inline]
148    #[must_use]
149    pub fn generate_cancel_id(order_id: &str) -> SmartString {
150        generate_report_id("cancel", order_id)
151    }
152
153    /// Generate a standard execution report ID for fills
154    #[inline]
155    #[must_use]
156    pub fn generate_fill_id(order_id: &str) -> SmartString {
157        generate_report_id("fill", order_id)
158    }
159
160    /// Generate a standard execution report ID for modifications
161    #[inline]
162    #[must_use]
163    pub fn generate_modify_id(order_id: &str) -> SmartString {
164        generate_report_id("modify", order_id)
165    }
166
167    /// Generate an exchange-specific report ID
168    #[inline]
169    #[must_use]
170    pub fn generate_exchange_report_id(exchange: &str, suffix: &str) -> SmartString {
171        generate_report_id(exchange, suffix)
172    }
173
174    /// Generate a WebSocket-specific report ID
175    #[inline]
176    #[must_use]
177    pub fn generate_ws_report_id(operation: &str, suffix: &str) -> SmartString {
178        let mut id = SmartString::new();
179        id.push_str("ws_");
180        id.push_str(operation);
181        id.push('_');
182        id.push_str(suffix);
183        id
184    }
185
186    /// Generate an exchange-specific order ID for WebSocket operations
187    #[inline]
188    #[must_use]
189    pub fn generate_exchange_order_id(exchange: &str) -> SmartString {
190        let uuid_str = Uuid::new_v4().to_string();
191        generate_report_id(&format!("{exchange}_order"), &uuid_str)
192    }
193
194    /// Generate a batch operation ID with an index
195    #[inline]
196    #[must_use]
197    pub fn generate_batch_id(prefix: &str, index: usize) -> SmartString {
198        let uuid_str = Uuid::new_v4().to_string();
199        let index_str = index.to_string();
200        generate_multi_part_id(&[prefix, "batch", &uuid_str, &index_str], '_')
201    }
202
203    /// Generate a timestamp-based ID for WebSocket operations
204    #[inline]
205    #[must_use]
206    pub fn generate_ws_timestamp_id(operation: &str, timestamp_ns: u64) -> SmartString {
207        let timestamp_str = timestamp_ns.to_string();
208        generate_ws_report_id(operation, &timestamp_str)
209    }
210
211    /// Generate an acknowledgment ID (standardized)
212    #[inline]
213    #[must_use]
214    pub fn generate_acknowledgment_id(order_id: &str) -> SmartString {
215        generate_report_id("ack", order_id)
216    }
217
218    /// Generate an execution ID
219    #[inline]
220    #[must_use]
221    pub fn generate_execution_id(order_id: &str) -> SmartString {
222        generate_report_id("exec", order_id)
223    }
224
225    /// Generate an exchange-specific request ID with counter
226    #[inline]
227    #[must_use]
228    pub fn generate_exchange_request_id(
229        _exchange: &str,
230        operation: &str,
231        counter: u64,
232    ) -> SmartString {
233        let counter_str = counter.to_string();
234        generate_multi_part_id(&[operation, &counter_str], '_')
235    }
236
237    /// Generate a UUID-based ID
238    #[inline]
239    #[must_use]
240    pub fn generate_uuid_id() -> SmartString {
241        Uuid::new_v4().to_string().into()
242    }
243
244    /// Generate a client order ID with prefix
245    #[inline]
246    #[must_use]
247    pub fn generate_client_order_id(suffix: &str) -> SmartString {
248        generate_report_id("order", suffix)
249    }
250
251    #[cfg(test)]
252    mod tests {
253        use super::*;
254
255        #[test]
256        fn test_generate_report_id() {
257            let id = generate_report_id("test", "123");
258            assert_eq!(id, "test_123");
259        }
260
261        #[test]
262        fn test_generate_ack_id() {
263            let id = generate_ack_id("order123");
264            assert_eq!(id, "ack_order123");
265        }
266
267        #[test]
268        fn test_generate_rejection_id() {
269            let id = generate_rejection_id("order456");
270            assert_eq!(id, "rej_order456");
271        }
272
273        #[test]
274        fn test_generate_multi_part_id() {
275            let id = generate_multi_part_id(&["part1", "part2", "part3"], '-');
276            assert_eq!(id, "part1-part2-part3");
277        }
278
279        #[test]
280        fn test_generate_ws_report_id() {
281            let id = generate_ws_report_id("place", "order123");
282            assert_eq!(id, "ws_place_order123");
283        }
284
285        #[test]
286        fn test_generate_exchange_report_id() {
287            let id = generate_exchange_report_id("binance", "execution123");
288            assert_eq!(id, "binance_execution123");
289        }
290
291        #[test]
292        fn test_generate_batch_id() {
293            let id = generate_batch_id("bybit", 0);
294            assert!(id.starts_with("bybit_batch_"));
295            assert!(id.ends_with("_0"));
296        }
297
298        #[test]
299        fn test_generate_ws_timestamp_id() {
300            let id = generate_ws_timestamp_id("error", 1234567890);
301            assert_eq!(id, "ws_error_1234567890");
302        }
303
304        #[test]
305        fn test_generate_acknowledgment_id() {
306            let id = generate_acknowledgment_id("order123");
307            assert_eq!(id, "ack_order123");
308        }
309
310        #[test]
311        fn test_generate_execution_id() {
312            let id = generate_execution_id("order456");
313            assert_eq!(id, "exec_order456");
314        }
315
316        #[test]
317        fn test_generate_exchange_request_id() {
318            let id = generate_exchange_request_id("bybit", "cancel", 42);
319            assert_eq!(id, "cancel_42");
320        }
321
322        #[test]
323        fn test_generate_uuid_id() {
324            let id = generate_uuid_id();
325            // UUID should be 36 characters with dashes
326            assert_eq!(id.len(), 36);
327            assert!(id.contains('-'));
328        }
329
330        #[test]
331        fn test_generate_client_order_id() {
332            let id = generate_client_order_id("123");
333            assert_eq!(id, "order_123");
334        }
335    }
336}
337/// SIMD NaN-safe operations submodule
338pub mod simd_nan_safe {
339    // NaN-safe SIMD operations using the wide crate
340    //
341    // This module provides safe SIMD operations that correctly handle NaN values
342    // without performance penalties. All operations use hardware NaN propagation.
343
344    use wide::{f32x8, f64x4};
345
346    /// Safe division that returns NaN for invalid operations (0/0, x/0, NaN/x, x/NaN)
347    #[inline(always)]
348    #[must_use]
349    pub fn safe_divide_f64x4(a: f64x4, b: f64x4) -> f64x4 {
350        // Hardware automatically handles NaN propagation
351        a / b
352    }
353
354    /// Safe division for f32x8
355    #[inline(always)]
356    #[must_use]
357    pub fn safe_divide_f32x8(a: f32x8, b: f32x8) -> f32x8 {
358        a / b
359    }
360
361    /// Check if any element in the vector is NaN
362    #[inline(always)]
363    pub fn has_nan_f64x4(v: f64x4) -> bool {
364        v.is_nan().any()
365    }
366
367    /// Check if any element in f32x8 is NaN
368    #[inline(always)]
369    pub fn has_nan_f32x8(v: f32x8) -> bool {
370        v.is_nan().any()
371    }
372
373    /// Replace NaN values with zero
374    #[inline(always)]
375    #[must_use]
376    pub fn nan_to_zero_f64x4(v: f64x4) -> f64x4 {
377        let mask = !v.is_nan();
378        mask & v
379    }
380
381    /// Replace NaN values with zero for f32x8
382    #[inline(always)]
383    #[must_use]
384    pub fn nan_to_zero_f32x8(v: f32x8) -> f32x8 {
385        let mask = !v.is_nan();
386        mask & v
387    }
388
389    /// Replace NaN values with a default value
390    #[inline(always)]
391    #[must_use]
392    pub fn nan_to_default_f64x4(v: f64x4, default: f64) -> f64x4 {
393        let mask = v.is_nan();
394        let default_vec = f64x4::splat(default);
395        (!mask & v) | (mask & default_vec)
396    }
397
398    /// Replace NaN values with a default value for f32x8
399    #[inline(always)]
400    #[must_use]
401    pub fn nan_to_default_f32x8(v: f32x8, default: f32) -> f32x8 {
402        let mask = v.is_nan();
403        let default_vec = f32x8::splat(default);
404        (!mask & v) | (mask & default_vec)
405    }
406
407    /// Safe logarithm that returns NaN for negative values
408    #[inline(always)]
409    #[must_use]
410    pub fn safe_log_f64x4(v: f64x4) -> f64x4 {
411        // wide doesn't have log, so we need to use scalar operations
412        // This is a temporary solution until wide adds transcendental functions
413        let arr: [f64; 4] = v.into();
414        f64x4::from([arr[0].ln(), arr[1].ln(), arr[2].ln(), arr[3].ln()])
415    }
416
417    /// Safe square root (returns NaN for negative values)
418    #[inline(always)]
419    #[must_use]
420    pub fn safe_sqrt_f64x4(v: f64x4) -> f64x4 {
421        v.sqrt() // Hardware handles NaN for negative inputs
422    }
423
424    /// Clamp values to a range, preserving NaN
425    #[inline(always)]
426    #[must_use]
427    pub fn clamp_f64x4(v: f64x4, min: f64, max: f64) -> f64x4 {
428        let min_vec = f64x4::splat(min);
429        let max_vec = f64x4::splat(max);
430        v.max(min_vec).min(max_vec) // NaN propagates through min/max
431    }
432
433    /// Calculate mean of non-NaN values
434    #[inline(always)]
435    #[must_use]
436    pub fn mean_non_nan_f64x4(v: f64x4) -> f64 {
437        let arr: [f64; 4] = v.into();
438        let mut sum = 0.0;
439        let mut count = 0;
440
441        for val in arr.iter() {
442            if !val.is_nan() {
443                sum += val;
444                count += 1;
445            }
446        }
447
448        if count > 0 {
449            sum / count as f64
450        } else {
451            f64::NAN
452        }
453    }
454
455    #[cfg(test)]
456    mod tests {
457        use super::*;
458
459        #[test]
460        fn test_safe_divide() {
461            let a = f64x4::from([10.0, 20.0, 0.0, 30.0]);
462            let b = f64x4::from([2.0, 0.0, 0.0, 5.0]);
463            let result = safe_divide_f64x4(a, b);
464            let arr: [f64; 4] = result.into();
465
466            assert_eq!(arr[0], 5.0);
467            assert!(arr[1].is_infinite()); // 20/0 = inf
468            assert!(arr[2].is_nan()); // 0/0 = NaN
469            assert_eq!(arr[3], 6.0);
470        }
471
472        #[test]
473        fn test_nan_to_zero() {
474            let v = f64x4::from([1.0, f64::NAN, 3.0, f64::NAN]);
475            let result = nan_to_zero_f64x4(v);
476            let arr: [f64; 4] = result.into();
477
478            assert_eq!(arr[0], 1.0);
479            assert_eq!(arr[1], 0.0);
480            assert_eq!(arr[2], 3.0);
481            assert_eq!(arr[3], 0.0);
482        }
483
484        #[test]
485        fn test_nan_to_default() {
486            let v = f64x4::from([1.0, f64::NAN, 3.0, f64::NAN]);
487            let result = nan_to_default_f64x4(v, -1.0);
488            let arr: [f64; 4] = result.into();
489
490            assert_eq!(arr[0], 1.0);
491            assert_eq!(arr[1], -1.0);
492            assert_eq!(arr[2], 3.0);
493            assert_eq!(arr[3], -1.0);
494        }
495
496        #[test]
497        fn test_safe_sqrt() {
498            let v = f64x4::from([4.0, -1.0, 16.0, 0.0]);
499            let result = safe_sqrt_f64x4(v);
500            let arr: [f64; 4] = result.into();
501
502            assert_eq!(arr[0], 2.0);
503            assert!(arr[1].is_nan()); // sqrt(-1) = NaN
504            assert_eq!(arr[2], 4.0);
505            assert_eq!(arr[3], 0.0);
506        }
507
508        #[test]
509        fn test_mean_non_nan() {
510            let v = f64x4::from([10.0, f64::NAN, 20.0, 30.0]);
511            let result = mean_non_nan_f64x4(v);
512            assert_eq!(result, 20.0); // (10 + 20 + 30) / 3
513
514            let all_nan = f64x4::from([f64::NAN; 4]);
515            let result_nan = mean_non_nan_f64x4(all_nan);
516            assert!(result_nan.is_nan());
517        }
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_f32x8_available_at_module_level() {
527        // This test demonstrates the need for f32x8 to be available at the module level
528        // for external users who want to use simd_nan_safe functions
529        use super::simd_nan_safe::*;
530
531        // This should work without needing to import f32x8 separately
532        let a = f32x8::from([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
533        let b = f32x8::from([2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0]);
534
535        let result = safe_divide_f32x8(a, b);
536        let expected = f32x8::from([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]);
537
538        let result_array: [f32; 8] = result.into();
539        let expected_array: [f32; 8] = expected.into();
540
541        for i in 0..8 {
542            assert!((result_array[i] - expected_array[i]).abs() < f32::EPSILON);
543        }
544    }
545
546    #[test]
547    fn test_smartstring_from_literal_conversions() {
548        // Test that .into() works for string literals
549        let s1: SmartString = "test".into();
550        assert_eq!(s1, "test");
551
552        // Test that SmartString::from() also works
553        let s2 = SmartString::from("test");
554        assert_eq!(s2, "test");
555
556        // Both should produce the same result
557        assert_eq!(s1, s2);
558
559        // Test with longer strings
560        let long: SmartString = "this is a longer string that might use heap allocation".into();
561        assert_eq!(
562            long,
563            "this is a longer string that might use heap allocation"
564        );
565    }
566
567    #[test]
568    fn test_to_string_into_conversions() {
569        #[derive(Debug)]
570        struct TestStruct {
571            value: i32,
572        }
573
574        impl std::fmt::Display for TestStruct {
575            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576                write!(f, "TestStruct({})", self.value)
577            }
578        }
579
580        let test = TestStruct { value: 42 };
581
582        // Test .to_string().into() pattern
583        let s1: SmartString = test.to_string().into();
584        assert_eq!(s1, "TestStruct(42)");
585
586        // Test direct .into() doesn't work for Display types
587        // let s2: SmartString = test.into(); // This should not compile
588
589        // But we can use format! macro
590        let s3: SmartString = format!("{test}").into();
591        assert_eq!(s3, "TestStruct(42)");
592    }
593
594    #[test]
595    fn test_parse_symbol_pair() {
596        assert_eq!(
597            parse_symbol_pair("BTC-USDT"),
598            Some(("BTC".into(), "USDT".into()))
599        );
600        assert_eq!(
601            parse_symbol_pair("ETH_BTC"),
602            Some(("ETH".into(), "BTC".into()))
603        );
604        assert_eq!(
605            parse_symbol_pair("BTC/USD"),
606            Some(("BTC".into(), "USD".into()))
607        );
608        assert_eq!(
609            parse_symbol_pair("BTCUSDT"),
610            Some(("BTC".into(), "USDT".into()))
611        );
612        assert_eq!(
613            parse_symbol_pair("ETHBTC"),
614            Some(("ETH".into(), "BTC".into()))
615        );
616    }
617
618    #[test]
619    fn test_format_decimal() {
620        assert_eq!(format_decimal(Decimal::from_str("1.00000").unwrap()), "1");
621        assert_eq!(format_decimal(Decimal::from_str("1.50000").unwrap()), "1.5");
622        assert_eq!(
623            format_decimal(Decimal::from_str("1.12345").unwrap()),
624            "1.12345"
625        );
626    }
627
628    #[test]
629    fn test_overflow_checks_configuration() {
630        // This test documents the overflow checks configuration and ensures
631        // we're handling financial calculations safely
632        //
633        // Development: overflow-checks = true (catch bugs early)
634        // Release: overflow-checks = false (HFT performance)
635
636        // Test basic arithmetic that should work in both profiles
637        let result = 100i64 + 200i64;
638        assert_eq!(result, 300i64);
639
640        // Test wrapping arithmetic that explicitly handles overflow
641        let safe_result = i64::MAX.wrapping_add(1);
642        assert_eq!(safe_result, i64::MIN); // Expected wrap-around behavior
643
644        // Test that we use rust_decimal for financial calculations
645        let financial_calc = Decimal::from_str("999999999999999999.99").unwrap();
646        let result = financial_calc + Decimal::from_str("0.01").unwrap();
647        assert!(result > financial_calc);
648    }
649}