1pub(crate) mod logger;
3
4use chrono::{DateTime, Utc};
5use base64::prelude::*;
6use rand::{rng, Rng};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9pub 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
16pub fn generate_timestamp() -> u64 {
18 SystemTime::now()
19 .duration_since(UNIX_EPOCH)
20 .unwrap()
21 .as_millis() as u64
22}
23
24pub fn format_fix_time(time: DateTime<Utc>) -> String {
26 time.format("%Y%m%d-%H:%M:%S%.3f").to_string()
27}
28
29pub 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
35pub 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
41pub 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
56pub fn generate_client_order_id(prefix: &str) -> String {
58 format!("{}_{}", prefix, generate_timestamp())
59}
60
61pub fn format_price(price: f64, precision: usize) -> String {
63 format!("{:.precision$}", price, precision = precision)
64}
65
66pub fn format_quantity(quantity: f64, precision: usize) -> String {
68 format!("{:.precision$}", quantity, precision = precision)
69}
70
71pub fn parse_decimal(decimal_str: &str) -> Result<f64, std::num::ParseFloatError> {
73 decimal_str.parse::<f64>()
74}
75
76pub fn escape_fix_value(value: &str) -> String {
78 value.replace('\x01', "\\001") }
80
81pub fn unescape_fix_value(value: &str) -> String {
83 value.replace("\\001", "\x01") }
85
86pub 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
93pub 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
101pub 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
111pub 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
121pub fn validate_instrument_name(instrument: &str) -> bool {
123 if instrument.is_empty() {
126 return false;
127 }
128
129 if !instrument.contains('-') {
131 return false;
132 }
133
134 let valid_currencies = ["BTC", "ETH", "USD", "USDC"];
136 let starts_with_valid_currency = valid_currencies.iter()
137 .any(|¤cy| instrument.starts_with(currency));
138
139 starts_with_valid_currency
140}
141
142pub 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
151pub 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 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}