alpaca_fix/
codec.rs

1//! FIX message encoding and decoding.
2
3use crate::config::FixVersion;
4use crate::error::{FixError, Result};
5use std::collections::HashMap;
6
7/// FIX field separator (SOH character).
8pub const SOH: char = '\x01';
9
10/// Common FIX tag numbers.
11pub mod tags {
12    /// BeginString (FIX version).
13    pub const BEGIN_STRING: u32 = 8;
14    /// Body length.
15    pub const BODY_LENGTH: u32 = 9;
16    /// Message type.
17    pub const MSG_TYPE: u32 = 35;
18    /// Sender CompID.
19    pub const SENDER_COMP_ID: u32 = 49;
20    /// Target CompID.
21    pub const TARGET_COMP_ID: u32 = 56;
22    /// Message sequence number.
23    pub const MSG_SEQ_NUM: u32 = 34;
24    /// Sending time.
25    pub const SENDING_TIME: u32 = 52;
26    /// Checksum.
27    pub const CHECKSUM: u32 = 10;
28    /// Client order ID.
29    pub const CL_ORD_ID: u32 = 11;
30    /// Order ID.
31    pub const ORDER_ID: u32 = 37;
32    /// Original client order ID.
33    pub const ORIG_CL_ORD_ID: u32 = 41;
34    /// Execution ID.
35    pub const EXEC_ID: u32 = 17;
36    /// Execution type.
37    pub const EXEC_TYPE: u32 = 150;
38    /// Order status.
39    pub const ORD_STATUS: u32 = 39;
40    /// Symbol.
41    pub const SYMBOL: u32 = 55;
42    /// Side.
43    pub const SIDE: u32 = 54;
44    /// Order type.
45    pub const ORD_TYPE: u32 = 40;
46    /// Order quantity.
47    pub const ORDER_QTY: u32 = 38;
48    /// Price.
49    pub const PRICE: u32 = 44;
50    /// Stop price.
51    pub const STOP_PX: u32 = 99;
52    /// Time in force.
53    pub const TIME_IN_FORCE: u32 = 59;
54    /// Last quantity.
55    pub const LAST_QTY: u32 = 32;
56    /// Last price.
57    pub const LAST_PX: u32 = 31;
58    /// Cumulative quantity.
59    pub const CUM_QTY: u32 = 14;
60    /// Average price.
61    pub const AVG_PX: u32 = 6;
62    /// Leaves quantity.
63    pub const LEAVES_QTY: u32 = 151;
64    /// Text.
65    pub const TEXT: u32 = 58;
66    /// Account.
67    pub const ACCOUNT: u32 = 1;
68    /// Heartbeat interval.
69    pub const HEART_BT_INT: u32 = 108;
70    /// Encrypt method.
71    pub const ENCRYPT_METHOD: u32 = 98;
72    /// Reset sequence number flag.
73    pub const RESET_SEQ_NUM_FLAG: u32 = 141;
74    /// Test request ID.
75    pub const TEST_REQ_ID: u32 = 112;
76    /// Begin sequence number.
77    pub const BEGIN_SEQ_NO: u32 = 7;
78    /// End sequence number.
79    pub const END_SEQ_NO: u32 = 16;
80    /// Market data request ID.
81    pub const MD_REQ_ID: u32 = 262;
82    /// Subscription request type.
83    pub const SUBSCRIPTION_REQUEST_TYPE: u32 = 263;
84    /// Market depth.
85    pub const MARKET_DEPTH: u32 = 264;
86    /// MD entry type.
87    pub const MD_ENTRY_TYPE: u32 = 269;
88    /// MD entry price.
89    pub const MD_ENTRY_PX: u32 = 270;
90    /// MD entry size.
91    pub const MD_ENTRY_SIZE: u32 = 271;
92}
93
94/// Raw FIX message representation.
95#[derive(Debug, Clone)]
96pub struct FixMessage {
97    /// Message fields as tag-value pairs.
98    pub fields: HashMap<u32, String>,
99    /// Original raw message.
100    pub raw: String,
101}
102
103impl FixMessage {
104    /// Create a new empty message.
105    #[must_use]
106    pub fn new() -> Self {
107        Self {
108            fields: HashMap::new(),
109            raw: String::new(),
110        }
111    }
112
113    /// Get a field value by tag.
114    #[must_use]
115    pub fn get(&self, tag: u32) -> Option<&str> {
116        self.fields.get(&tag).map(String::as_str)
117    }
118
119    /// Get message type.
120    #[must_use]
121    pub fn msg_type(&self) -> Option<&str> {
122        self.get(tags::MSG_TYPE)
123    }
124
125    /// Set a field value.
126    pub fn set(&mut self, tag: u32, value: impl Into<String>) {
127        self.fields.insert(tag, value.into());
128    }
129
130    /// Check if field exists.
131    #[must_use]
132    pub fn has(&self, tag: u32) -> bool {
133        self.fields.contains_key(&tag)
134    }
135}
136
137impl Default for FixMessage {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143/// FIX message encoder.
144#[derive(Debug)]
145pub struct FixEncoder {
146    version: FixVersion,
147    sender_comp_id: String,
148    target_comp_id: String,
149}
150
151impl FixEncoder {
152    /// Create a new encoder.
153    #[must_use]
154    pub fn new(version: FixVersion, sender_comp_id: &str, target_comp_id: &str) -> Self {
155        Self {
156            version,
157            sender_comp_id: sender_comp_id.to_string(),
158            target_comp_id: target_comp_id.to_string(),
159        }
160    }
161
162    /// Encode a message to FIX format.
163    pub fn encode(&self, msg_type: &str, seq_num: u64, fields: &[(u32, String)]) -> String {
164        let sending_time = chrono::Utc::now().format("%Y%m%d-%H:%M:%S%.3f").to_string();
165
166        // Build body (everything except BeginString, BodyLength, and CheckSum)
167        let mut body = String::new();
168        body.push_str(&format!("{}={}{}", tags::MSG_TYPE, msg_type, SOH));
169        body.push_str(&format!(
170            "{}={}{}",
171            tags::SENDER_COMP_ID,
172            self.sender_comp_id,
173            SOH
174        ));
175        body.push_str(&format!(
176            "{}={}{}",
177            tags::TARGET_COMP_ID,
178            self.target_comp_id,
179            SOH
180        ));
181        body.push_str(&format!("{}={}{}", tags::MSG_SEQ_NUM, seq_num, SOH));
182        body.push_str(&format!("{}={}{}", tags::SENDING_TIME, sending_time, SOH));
183
184        for (tag, value) in fields {
185            body.push_str(&format!("{}={}{}", tag, value, SOH));
186        }
187
188        // Build header
189        let header = format!(
190            "{}={}{}{}={}{}",
191            tags::BEGIN_STRING,
192            self.version.begin_string(),
193            SOH,
194            tags::BODY_LENGTH,
195            body.len(),
196            SOH
197        );
198
199        // Calculate checksum
200        let checksum = Self::calculate_checksum(&format!("{}{}", header, body));
201
202        format!(
203            "{}{}{}={:03}{}",
204            header,
205            body,
206            tags::CHECKSUM,
207            checksum,
208            SOH
209        )
210    }
211
212    /// Calculate FIX checksum.
213    fn calculate_checksum(data: &str) -> u8 {
214        data.bytes().fold(0u32, |acc, b| acc + b as u32) as u8
215    }
216}
217
218/// FIX message decoder.
219#[derive(Debug, Default)]
220pub struct FixDecoder;
221
222impl FixDecoder {
223    /// Create a new decoder.
224    #[must_use]
225    pub fn new() -> Self {
226        Self
227    }
228
229    /// Decode a FIX message.
230    pub fn decode(&self, data: &str) -> Result<FixMessage> {
231        let mut msg = FixMessage::new();
232        msg.raw = data.to_string();
233
234        for field in data.split(SOH) {
235            if field.is_empty() {
236                continue;
237            }
238
239            let parts: Vec<&str> = field.splitn(2, '=').collect();
240            if parts.len() != 2 {
241                return Err(FixError::Decoding(format!("invalid field: {}", field)));
242            }
243
244            let tag: u32 = parts[0]
245                .parse()
246                .map_err(|_| FixError::Decoding(format!("invalid tag: {}", parts[0])))?;
247            msg.fields.insert(tag, parts[1].to_string());
248        }
249
250        // Validate required fields
251        if !msg.has(tags::BEGIN_STRING) {
252            return Err(FixError::InvalidMessage("missing BeginString".to_string()));
253        }
254        if !msg.has(tags::MSG_TYPE) {
255            return Err(FixError::InvalidMessage("missing MsgType".to_string()));
256        }
257
258        Ok(msg)
259    }
260
261    /// Validate message checksum.
262    #[must_use]
263    pub fn validate_checksum(&self, data: &str) -> bool {
264        // Find checksum field position
265        if let Some(pos) = data.rfind(&format!("{}=", tags::CHECKSUM)) {
266            let body = &data[..pos];
267            let checksum_str = &data[pos + 3..data.len() - 1]; // Skip "10=" and trailing SOH
268
269            if let Ok(expected) = checksum_str.parse::<u8>() {
270                let calculated = body.bytes().fold(0u32, |acc, b| acc + b as u32) as u8;
271                return calculated == expected;
272            }
273        }
274        false
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_fix_message_fields() {
284        let mut msg = FixMessage::new();
285        msg.set(tags::SYMBOL, "AAPL");
286        msg.set(tags::SIDE, "1");
287
288        assert_eq!(msg.get(tags::SYMBOL), Some("AAPL"));
289        assert_eq!(msg.get(tags::SIDE), Some("1"));
290        assert!(msg.has(tags::SYMBOL));
291        assert!(!msg.has(tags::PRICE));
292    }
293
294    #[test]
295    fn test_fix_encoder() {
296        let encoder = FixEncoder::new(FixVersion::Fix44, "SENDER", "TARGET");
297        let fields = vec![
298            (tags::SYMBOL, "AAPL".to_string()),
299            (tags::SIDE, "1".to_string()),
300        ];
301        let encoded = encoder.encode("D", 1, &fields);
302
303        assert!(encoded.contains("8=FIX.4.4"));
304        assert!(encoded.contains("35=D"));
305        assert!(encoded.contains("49=SENDER"));
306        assert!(encoded.contains("56=TARGET"));
307        assert!(encoded.contains("55=AAPL"));
308    }
309
310    #[test]
311    fn test_fix_decoder() {
312        let decoder = FixDecoder::new();
313        let raw = "8=FIX.4.4\x0135=D\x0149=SENDER\x0156=TARGET\x0155=AAPL\x0110=000\x01";
314        let msg = decoder.decode(raw).unwrap();
315
316        assert_eq!(msg.get(tags::BEGIN_STRING), Some("FIX.4.4"));
317        assert_eq!(msg.msg_type(), Some("D"));
318        assert_eq!(msg.get(tags::SYMBOL), Some("AAPL"));
319    }
320}