nautilus_coinbase_intx/fix/
messages.rs
1#![allow(dead_code)]
18
19use chrono::{DateTime, Utc};
20use indexmap::IndexMap;
21
22pub mod fix_tag {
24 pub const BEGIN_STRING: u32 = 8; pub const BODY_LENGTH: u32 = 9; pub const MSG_TYPE: u32 = 35; pub const SENDER_COMP_ID: u32 = 49; pub const TARGET_COMP_ID: u32 = 56; pub const MSG_SEQ_NUM: u32 = 34; pub const SENDING_TIME: u32 = 52; pub const CHECKSUM: u32 = 10; pub const ENCRYPT_METHOD: u32 = 98; pub const HEART_BT_INT: u32 = 108; pub const RESET_SEQ_NUM_FLAG: u32 = 141; pub const USERNAME: u32 = 553; pub const PASSWORD: u32 = 554; pub const CL_ORD_ID: u32 = 11; pub const ORIG_CL_ORD_ID: u32 = 41; pub const TRD_MATCH_ID: u32 = 880; pub const EXEC_ID: u32 = 17; pub const EXEC_TRANS_TYPE: u32 = 20; pub const ORDER_ID: u32 = 37; pub const EXEC_TYPE: u32 = 150; pub const ORD_STATUS: u32 = 39; pub const ORD_REJ_REASON: u32 = 103; pub const SYMBOL: u32 = 55; pub const SIDE: u32 = 54; pub const ORDER_QTY: u32 = 38; pub const ORD_TYPE: u32 = 40; pub const PRICE: u32 = 44; pub const STOP_PX: u32 = 99; pub const STOP_LIMIT_PX: u32 = 3040; pub const CURRENCY: u32 = 15; pub const TIME_IN_FORCE: u32 = 59; pub const EXPIRE_TIME: u32 = 126; pub const EXEC_INST: u32 = 18; pub const LAST_QTY: u32 = 32; pub const LAST_PX: u32 = 31; pub const LEAVES_QTY: u32 = 151; pub const CUM_QTY: u32 = 14; pub const AVG_PX: u32 = 6; pub const TRANSACT_TIME: u32 = 60; pub const TEXT: u32 = 58; pub const LAST_LIQUIDITY_IND: u32 = 851; pub const NO_PARTY_IDS: u32 = 453; pub const PARTY_ID: u32 = 448; pub const PARTY_ID_SOURCE: u32 = 447; pub const PARTY_ROLE: u32 = 452; pub const NO_MISC_FEES: u32 = 136; pub const MISC_FEE_AMT: u32 = 137; pub const MISC_FEE_CURR: u32 = 138; pub const MISC_FEE_TYPE: u32 = 139; pub const SELF_TRADE_PREVENTION_STRATEGY: u32 = 8000; pub const TARGET_STRATEGY: u32 = 847; pub const DEFAULT_APPL_VER_ID: u32 = 1137; }
88
89pub(crate) mod fix_message_type {
91 pub const HEARTBEAT: &str = "0";
92 pub const TEST_REQUEST: &str = "1";
93 pub const RESEND_REQUEST: &str = "2";
94 pub const REJECT: &str = "3";
95 pub const SEQUENCE_RESET: &str = "4";
96 pub const LOGOUT: &str = "5";
97 pub const EXECUTION_REPORT: &str = "8";
98 pub const ORDER_CANCEL_REJECT: &str = "9";
99 pub const LOGON: &str = "A";
100 pub const NEWS: &str = "B";
101 pub const EMAIL: &str = "C";
102 pub const NEW_ORDER_SINGLE: &str = "D";
103 pub const ORDER_CANCEL_REQUEST: &str = "F";
104 pub const ORDER_CANCEL_REPLACE_REQUEST: &str = "G";
105 pub const ORDER_STATUS_REQUEST: &str = "H";
106 pub const BUSINESS_MESSAGE_REJECT: &str = "j";
107}
108
109pub(crate) mod fix_exec_type {
111 pub const NEW: &str = "0";
112 pub const PARTIAL_FILL: &str = "1";
113 pub const FILL: &str = "2";
114 pub const CANCELED: &str = "4";
115 pub const REPLACED: &str = "5";
116 pub const PENDING_CANCEL: &str = "6";
117 pub const REJECTED: &str = "8";
118 pub const PENDING_NEW: &str = "A";
119 pub const EXPIRED: &str = "C";
120 pub const PENDING_REPLACE: &str = "E";
121 pub const TRADE: &str = "F"; pub const STOP_TRIGGERED: &str = "L";
123}
124
125pub(crate) const FIX_DELIMITER: u8 = b'\x01';
126
127#[derive(Debug, Clone)]
129pub(crate) struct FixMessage {
130 fields: IndexMap<u32, String>,
131}
132
133impl FixMessage {
134 pub(crate) fn new(
136 msg_type: &str,
137 seq_num: usize,
138 sender_comp_id: &str,
139 target_comp_id: &str,
140 now: &DateTime<Utc>,
141 ) -> Self {
142 let mut fields = IndexMap::new();
143
144 fields.insert(fix_tag::MSG_TYPE, msg_type.to_string());
146 fields.insert(fix_tag::SENDER_COMP_ID, sender_comp_id.to_string());
147 fields.insert(fix_tag::TARGET_COMP_ID, target_comp_id.to_string());
148 fields.insert(fix_tag::MSG_SEQ_NUM, seq_num.to_string());
149
150 let timestamp = now.format("%Y%m%d-%H:%M:%S%.6f").to_string();
152 fields.insert(fix_tag::SENDING_TIME, timestamp);
153
154 Self { fields }
155 }
156
157 pub(crate) fn msg_type(&self) -> Option<&str> {
159 self.get_field(fix_tag::MSG_TYPE)
160 }
161
162 pub(crate) fn msg_seq_num(&self) -> Option<usize> {
164 self.get_field(fix_tag::MSG_SEQ_NUM)
165 .and_then(|s| s.parse::<usize>().ok())
166 }
167
168 pub fn get_field(&self, tag: u32) -> Option<&str> {
170 self.fields.get(&tag).map(|s| s.as_str())
171 }
172
173 pub fn add_field(&mut self, tag: u32, value: impl Into<String>) -> &mut Self {
175 self.fields.insert(tag, value.into());
176 self
177 }
178
179 pub(crate) fn parse(data: &[u8]) -> Result<Self, String> {
181 const DELIMITER: char = '\x01'; let data_str = std::str::from_utf8(data).map_err(|e| format!("Invalid UTF-8: {e}"))?;
184
185 let mut fields = IndexMap::new();
186
187 for field_str in data_str.split(DELIMITER) {
188 if field_str.is_empty() {
189 continue;
190 }
191
192 let parts: Vec<&str> = field_str.splitn(2, '=').collect();
193 if parts.len() != 2 {
194 return Err(format!("Invalid field: {field_str}"));
195 }
196
197 let tag = parts[0]
198 .parse::<u32>()
199 .map_err(|e| format!("Invalid tag: {e}"))?;
200 let value = parts[1].to_string();
201
202 fields.insert(tag, value);
203 }
204
205 Ok(Self { fields })
206 }
207
208 pub(crate) fn get_field_checked(&self, tag: u32) -> anyhow::Result<&str> {
214 self.get_field(tag)
215 .ok_or(anyhow::anyhow!("Missing tag {tag}"))
216 }
217
218 fn set_field(&mut self, tag: u32, value: impl Into<String>) -> &mut Self {
220 self.fields.insert(tag, value.into());
221 self
222 }
223
224 pub(crate) fn to_bytes(&self) -> Vec<u8> {
226 let mut buffer = Vec::new();
227
228 let begin_string = self.get_field(fix_tag::BEGIN_STRING).unwrap_or("FIXT.1.1");
230 buffer.extend_from_slice(format!("{}={}", fix_tag::BEGIN_STRING, begin_string).as_bytes());
231 buffer.push(FIX_DELIMITER);
232
233 let mut body_buffer = Vec::new();
234
235 for (&tag, value) in &self.fields {
237 body_buffer.extend_from_slice(format!("{tag}={value}").as_bytes());
238 body_buffer.push(FIX_DELIMITER);
239 }
240
241 let body_length = body_buffer.len();
243 buffer.extend_from_slice(format!("{}={}", fix_tag::BODY_LENGTH, body_length).as_bytes());
244 buffer.push(FIX_DELIMITER);
245
246 buffer.extend_from_slice(&body_buffer);
248
249 let checksum: u32 = buffer.iter().map(|&b| b as u32).sum::<u32>() % 256;
251 buffer.extend_from_slice(format!("{}={:03}", fix_tag::CHECKSUM, checksum).as_bytes());
252 buffer.push(FIX_DELIMITER);
253
254 buffer
255 }
256
257 #[allow(clippy::too_many_arguments)]
259 pub(crate) fn create_logon(
260 seq_num: usize,
261 sender_comp_id: &str,
262 target_comp_id: &str,
263 heartbeat_interval: u64,
264 username: &str,
265 password: &str,
266 text: &str,
267 timestamp: &DateTime<Utc>,
268 ) -> Self {
269 let mut msg = Self::new(
270 fix_message_type::LOGON,
271 seq_num,
272 sender_comp_id,
273 target_comp_id,
274 timestamp,
275 );
276
277 msg.add_field(fix_tag::ENCRYPT_METHOD, "0") .add_field(fix_tag::HEART_BT_INT, heartbeat_interval.to_string())
279 .add_field(fix_tag::RESET_SEQ_NUM_FLAG, "Y")
280 .add_field(fix_tag::USERNAME, username)
281 .add_field(fix_tag::PASSWORD, password)
282 .add_field(fix_tag::TEXT, text)
283 .add_field(fix_tag::DEFAULT_APPL_VER_ID, "9");
284
285 msg
286 }
287
288 pub(crate) fn create_heartbeat(
290 seq_num: usize,
291 sender_comp_id: &str,
292 target_comp_id: &str,
293 timestamp: &DateTime<Utc>,
294 ) -> Self {
295 Self::new(
296 fix_message_type::HEARTBEAT,
297 seq_num,
298 sender_comp_id,
299 target_comp_id,
300 timestamp,
301 )
302 }
303
304 pub(crate) fn create_logout(
306 seq_num: usize,
307 sender_comp_id: &str,
308 target_comp_id: &str,
309 text: Option<&str>,
310 timestamp: &DateTime<Utc>,
311 ) -> Self {
312 let mut msg = Self::new(
313 fix_message_type::LOGOUT,
314 seq_num,
315 sender_comp_id,
316 target_comp_id,
317 timestamp,
318 );
319
320 if let Some(text) = text {
321 msg.add_field(fix_tag::TEXT, text);
322 }
323
324 msg
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use chrono::TimeZone;
331
332 use super::*;
333
334 #[test]
335 fn test_fix_message_to_bytes_simple() {
336 let timestamp = Utc.with_ymd_and_hms(2025, 3, 22, 12, 34, 56).unwrap();
337 let mut msg = FixMessage::new(fix_message_type::LOGON, 1, "SENDER", "TARGET", ×tamp);
338
339 msg.add_field(fix_tag::ENCRYPT_METHOD, "0")
340 .add_field(fix_tag::HEART_BT_INT, "10")
341 .add_field(fix_tag::RESET_SEQ_NUM_FLAG, "Y");
342
343 let bytes = msg.to_bytes();
344 let message = String::from_utf8(bytes).unwrap();
345 let expected = "8=FIXT.1.1\x019=76\x0135=A\x0149=SENDER\x0156=TARGET\x0134=1\x0152=20250322-12:34:56.000000\x0198=0\x01108=10\x01141=Y\x0110=137\x01";
346
347 assert_eq!(message, expected);
348 }
349
350 #[test]
351 fn test_fix_message_to_bytes_complete_logon() {
352 let timestamp = Utc.with_ymd_and_hms(2025, 3, 22, 12, 34, 56).unwrap();
353 let msg = FixMessage::create_logon(
354 1,
355 "SENDER",
356 "TARGET",
357 30,
358 "username",
359 "password",
360 "signature",
361 ×tamp,
362 );
363
364 let bytes = msg.to_bytes();
365 let message = String::from_utf8(bytes).unwrap();
366 let expected = "8=FIXT.1.1\x019=122\x0135=A\x0149=SENDER\x0156=TARGET\x0134=1\x0152=20250322-12:34:56.000000\x0198=0\x01108=30\x01141=Y\x01553=username\x01554=password\x0158=signature\x011137=9\x0110=253\x01";
367
368 assert_eq!(message, expected);
369 }
370}