deribit_fix/utils/
mod.rs

1//! Utility functions for the Deribit FIX framework
2pub(crate) mod logger;
3
4use chrono::{DateTime, Utc};
5use base64::prelude::*;
6use rand::{rng, Rng};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// Generate a cryptographically secure random nonce
10pub fn generate_nonce(length: usize) -> String {
11    let mut rng = rng();
12    let bytes: Vec<u8> = (0..length).map(|_| rng.random()).collect();
13    base64::prelude::BASE64_STANDARD.encode(bytes)
14}
15
16/// Generate a timestamp in milliseconds since Unix epoch
17pub fn generate_timestamp() -> u64 {
18    SystemTime::now()
19        .duration_since(UNIX_EPOCH)
20        .unwrap()
21        .as_millis() as u64
22}
23
24/// Format a DateTime for FIX SendingTime field (YYYYMMDD-HH:MM:SS.sss)
25pub fn format_fix_time(time: DateTime<Utc>) -> String {
26    time.format("%Y%m%d-%H:%M:%S%.3f").to_string()
27}
28
29/// Parse a FIX time string to DateTime
30pub fn parse_fix_time(time_str: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
31    DateTime::parse_from_str(&format!("{}+00:00", time_str), "%Y%m%d-%H:%M:%S%.3f%z")
32        .map(|dt| dt.with_timezone(&Utc))
33}
34
35/// Calculate FIX checksum for a message
36pub fn calculate_checksum(message: &str) -> u8 {
37    let sum: u32 = message.bytes().map(|b| b as u32).sum();
38    (sum % 256) as u8
39}
40
41/// Validate FIX checksum
42pub fn validate_checksum(message: &str) -> bool {
43    if let Some(checksum_pos) = message.rfind("10=") {
44        let message_without_checksum = &message[..checksum_pos];
45        let expected_checksum = calculate_checksum(message_without_checksum);
46        
47        if let Some(checksum_str) = message[checksum_pos + 3..].split('\x01').next() {
48            if let Ok(actual_checksum) = checksum_str.parse::<u8>() {
49                return expected_checksum == actual_checksum;
50            }
51        }
52    }
53    false
54}
55
56/// Generate a unique client order ID
57pub fn generate_client_order_id(prefix: &str) -> String {
58    format!("{}_{}", prefix, generate_timestamp())
59}
60
61/// Convert price to FIX decimal format
62pub fn format_price(price: f64, precision: usize) -> String {
63    format!("{:.precision$}", price, precision = precision)
64}
65
66/// Convert quantity to FIX decimal format
67pub fn format_quantity(quantity: f64, precision: usize) -> String {
68    format!("{:.precision$}", quantity, precision = precision)
69}
70
71/// Parse FIX decimal string to f64
72pub fn parse_decimal(decimal_str: &str) -> Result<f64, std::num::ParseFloatError> {
73    decimal_str.parse::<f64>()
74}
75
76/// Escape special characters in FIX field values
77pub fn escape_fix_value(value: &str) -> String {
78    value.replace('\x01', "\\001") // SOH
79}
80
81/// Unescape special characters in FIX field values
82pub fn unescape_fix_value(value: &str) -> String {
83    value.replace("\\001", "\x01") // SOH
84}
85
86/// Generate a random request ID
87pub fn generate_request_id(prefix: &str) -> String {
88    let mut rng = rng();
89    let random_part: u32 = rng.random();
90    format!("{}_{}", prefix, random_part)
91}
92
93/// Convert side enum to FIX side value
94pub fn side_to_fix(side: crate::client::OrderSide) -> &'static str {
95    match side {
96        crate::client::OrderSide::Buy => "1",
97        crate::client::OrderSide::Sell => "2",
98    }
99}
100
101/// Convert order type enum to FIX order type value
102pub fn order_type_to_fix(order_type: crate::client::OrderType) -> &'static str {
103    match order_type {
104        crate::client::OrderType::Market => "1",
105        crate::client::OrderType::Limit => "2",
106        crate::client::OrderType::Stop => "3",
107        crate::client::OrderType::StopLimit => "4",
108    }
109}
110
111/// Convert time in force enum to FIX time in force value
112pub fn time_in_force_to_fix(tif: crate::client::TimeInForce) -> &'static str {
113    match tif {
114        crate::client::TimeInForce::Day => "0",
115        crate::client::TimeInForce::GoodTillCancel => "1",
116        crate::client::TimeInForce::ImmediateOrCancel => "3",
117        crate::client::TimeInForce::FillOrKill => "4",
118    }
119}
120
121/// Validate instrument name format for Deribit
122pub fn validate_instrument_name(instrument: &str) -> bool {
123    // Basic validation for Deribit instrument naming convention
124    // Examples: BTC-PERPETUAL, ETH-25DEC20-600-C, BTC-25DEC20
125    if instrument.is_empty() {
126        return false;
127    }
128    
129    // Must contain at least one dash
130    if !instrument.contains('-') {
131        return false;
132    }
133    
134    // Must start with a valid currency
135    let valid_currencies = ["BTC", "ETH", "USD", "USDC"];
136    let starts_with_valid_currency = valid_currencies.iter()
137        .any(|&currency| instrument.starts_with(currency));
138    
139    starts_with_valid_currency
140}
141
142/// Extract currency from instrument name
143pub fn extract_currency_from_instrument(instrument: &str) -> Option<&str> {
144    if let Some(dash_pos) = instrument.find('-') {
145        Some(&instrument[..dash_pos])
146    } else {
147        None
148    }
149}
150
151/// Format instrument name for Deribit
152pub fn format_deribit_instrument(
153    currency: &str,
154    expiry: Option<&str>,
155    strike: Option<f64>,
156    option_type: Option<&str>,
157) -> String {
158    let mut instrument = currency.to_string();
159    
160    if let Some(exp) = expiry {
161        instrument.push('-');
162        instrument.push_str(exp);
163        
164        if let Some(strike_price) = strike {
165            instrument.push('-');
166            instrument.push_str(&strike_price.to_string());
167            
168            if let Some(opt_type) = option_type {
169                instrument.push('-');
170                instrument.push_str(opt_type);
171            }
172        }
173    } else {
174        // Perpetual contract
175        instrument.push_str("-PERPETUAL");
176    }
177    
178    instrument
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_generate_nonce() {
187        let nonce1 = generate_nonce(32);
188        let nonce2 = generate_nonce(32);
189        
190        assert_ne!(nonce1, nonce2);
191        assert!(!nonce1.is_empty());
192        assert!(!nonce2.is_empty());
193    }
194
195    #[test]
196    fn test_checksum_calculation() {
197        let message = "8=FIX.4.4\x019=61\x0135=A\x0149=CLIENT\x0156=DERIBITSERVER\x0134=1\x01";
198        let checksum = calculate_checksum(message);
199        assert!(checksum <= 255); 
200    }
201
202    #[test]
203    fn test_instrument_validation() {
204        assert!(validate_instrument_name("BTC-PERPETUAL"));
205        assert!(validate_instrument_name("ETH-25DEC20-600-C"));
206        assert!(validate_instrument_name("BTC-25DEC20"));
207        assert!(!validate_instrument_name("INVALID"));
208        assert!(!validate_instrument_name(""));
209    }
210
211    #[test]
212    fn test_currency_extraction() {
213        assert_eq!(extract_currency_from_instrument("BTC-PERPETUAL"), Some("BTC"));
214        assert_eq!(extract_currency_from_instrument("ETH-25DEC20-600-C"), Some("ETH"));
215        assert_eq!(extract_currency_from_instrument("INVALID"), None);
216    }
217
218    #[test]
219    fn test_instrument_formatting() {
220        assert_eq!(
221            format_deribit_instrument("BTC", None, None, None),
222            "BTC-PERPETUAL"
223        );
224        assert_eq!(
225            format_deribit_instrument("ETH", Some("25DEC20"), Some(600.0), Some("C")),
226            "ETH-25DEC20-600-C"
227        );
228        assert_eq!(
229            format_deribit_instrument("BTC", Some("25DEC20"), None, None),
230            "BTC-25DEC20"
231        );
232    }
233}