Skip to main content

deribit_fix/message/
admin.rs

1//! Administrative FIX Messages
2//!
3//! This module implements administrative messages used for session management
4//! and connectivity testing in the FIX protocol. These messages are essential
5//! for maintaining reliable communication between trading counterparties.
6//!
7//! ## Message Types Implemented
8//!
9//! - **Heartbeat (0)**: Periodic keep-alive messages to maintain session connectivity
10//! - **Test Request (1)**: Request for heartbeat response to test connectivity  
11//! - **Resend Request (2)**: Request to resend specific messages by sequence number range
12//! - **Reject (3)**: Rejection of received messages due to validation errors
13//! - **Business Message Reject (j)**: Business-level rejection of application messages
14
15use crate::error::Result;
16use crate::message::MessageBuilder;
17use crate::model::message::FixMessage;
18use crate::model::types::MsgType;
19use chrono::Utc;
20use serde::{Deserialize, Serialize};
21
22/// Heartbeat message (MsgType = 0)
23///
24/// The Heartbeat message is used to monitor the status of the communication link.
25/// It is sent periodically to ensure the counterparty is still active and responsive.
26/// If no messages are received within the heartbeat interval, a Test Request should be sent.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Heartbeat {
29    /// TestReqID (112) - Optional field echoed from Test Request
30    /// Present only when responding to a Test Request message
31    pub test_req_id: Option<String>,
32}
33
34/// Business-level reject reason codes (FIX 4.4, tag 380)
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum BusinessRejectReason {
37    /// Other
38    Other = 0,
39    /// Unknown ID
40    UnknownId = 1,
41    /// Unknown Security
42    UnknownSecurity = 2,
43    /// Unsupported Message Type
44    UnsupportedMessageType = 3,
45    /// Application not available
46    ApplicationNotAvailable = 4,
47    /// Conditionally required field missing
48    ConditionallyRequiredFieldMissing = 5,
49    /// Not authorized
50    NotAuthorized = 6,
51    /// DeliverTo firm not available at this time
52    DeliverToFirmNotAvailableAtThisTime = 7,
53}
54
55/// Business Message Reject (MsgType = j)
56///
57/// This message is used to reject application-level (business) messages
58/// when they cannot be processed for business reasons.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct BusinessMessageReject {
61    /// RefMsgType (372) - Message type of the rejected application message (required)
62    pub ref_msg_type: String,
63
64    /// BusinessRejectReason (380) - Reason for rejection (required)
65    pub business_reject_reason: BusinessRejectReason,
66
67    /// BusinessRejectRefID (379) - ID from the rejected message for correlation (optional)
68    pub business_reject_ref_id: Option<String>,
69
70    /// Text (58) - Optional free-form text with details
71    pub text: Option<String>,
72}
73
74/// Test Request message (MsgType = 1)
75///
76/// The Test Request message is sent to force a Heartbeat response from the counterparty.
77/// It is typically used when no messages have been received within the expected heartbeat interval.
78/// The receiving party must respond with a Heartbeat message containing the same TestReqID.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct TestRequest {
81    /// TestReqID (112) - Unique identifier for this test request
82    /// Must be echoed back in the responding Heartbeat message
83    pub test_req_id: String,
84}
85
86/// Resend Request message (MsgType = 2)
87///
88/// The Resend Request message is sent to request retransmission of messages
89/// within a specified sequence number range. This is used for gap recovery
90/// when messages are detected as missing from the sequence.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ResendRequest {
93    /// BeginSeqNo (7) - Starting sequence number for resend range
94    pub begin_seq_no: u32,
95
96    /// EndSeqNo (16) - Ending sequence number for resend range
97    /// Set to 0 to request all messages from BeginSeqNo to current
98    pub end_seq_no: u32,
99}
100
101/// Sequence Reset message (MsgType = 4)
102///
103/// The Sequence Reset message is used to recover from an out-of-sequence condition,
104/// to reestablish a FIX session after a sequence loss. The MsgSeqNum(34) in the
105/// header is ignored.
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct SequenceReset {
108    /// NewSeqNo (36) - New sequence number to reset to
109    /// This can only increase the sequence number, never decrease it
110    pub new_seq_no: u32,
111
112    /// GapFillFlag (123) - Indicates if this is a gap fill
113    /// Y = Gap Fill message, N = Sequence Reset message
114    pub gap_fill_flag: Option<bool>,
115}
116
117/// Reject message (MsgType = 3)
118///
119/// The Reject message is sent when a received message cannot be processed
120/// due to validation errors, formatting issues, or other problems.
121/// It provides detailed information about why the message was rejected.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct Reject {
124    /// RefSeqNum (45) - Sequence number of the rejected message
125    pub ref_seq_num: u32,
126
127    /// RefTagID (371) - Tag number of the field that caused rejection
128    /// Optional field present when rejection is due to a specific field
129    pub ref_tag_id: Option<u32>,
130
131    /// RefMsgType (372) - Message type of the rejected message
132    /// Optional field to identify which message type was rejected
133    pub ref_msg_type: Option<String>,
134
135    /// SessionRejectReason (373) - Reason code for rejection
136    /// Standardized codes defined in FIX specification
137    pub session_reject_reason: Option<u32>,
138
139    /// Text (58) - Human-readable description of rejection reason
140    /// Optional free-form text providing additional details
141    pub text: Option<String>,
142}
143
144/// Session reject reason codes as defined in FIX specification
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
146pub enum SessionRejectReason {
147    /// Invalid tag number
148    InvalidTagNumber = 0,
149    /// Required tag missing
150    RequiredTagMissing = 1,
151    /// Tag not defined for this message type
152    TagNotDefinedForMessageType = 2,
153    /// Undefined tag
154    UndefinedTag = 3,
155    /// Tag specified without a value
156    TagSpecifiedWithoutValue = 4,
157    /// Value is incorrect (out of range) for this tag
158    ValueIncorrectForTag = 5,
159    /// Incorrect data format for value
160    IncorrectDataFormat = 6,
161    /// Decryption problem
162    DecryptionProblem = 7,
163    /// Signature problem
164    SignatureProblem = 8,
165    /// CompID problem
166    CompIdProblem = 9,
167    /// SendingTime accuracy problem
168    SendingTimeAccuracyProblem = 10,
169    /// Invalid MsgType
170    InvalidMsgType = 11,
171    /// XML validation error
172    XmlValidationError = 12,
173    /// Tag appears more than once
174    TagAppearsMoreThanOnce = 13,
175    /// Tag specified out of required order
176    TagSpecifiedOutOfOrder = 14,
177    /// Repeating group fields out of order
178    RepeatingGroupFieldsOutOfOrder = 15,
179    /// Incorrect NumInGroup count for repeating group
180    IncorrectNumInGroupCount = 16,
181    /// Non "data" value includes field delimiter
182    NonDataValueIncludesFieldDelimiter = 17,
183    /// Other
184    Other = 99,
185}
186
187// Implement JSON display for all message types
188impl_json_display!(Heartbeat);
189impl_json_display!(TestRequest);
190impl_json_display!(ResendRequest);
191impl_json_display!(SequenceReset);
192impl_json_display!(Reject);
193impl_json_display!(BusinessMessageReject);
194
195impl Heartbeat {
196    /// Create a new Heartbeat message without TestReqID (periodic heartbeat)
197    pub fn new() -> Self {
198        Self { test_req_id: None }
199    }
200
201    /// Create a new Heartbeat message responding to a Test Request
202    pub fn new_response(test_req_id: String) -> Self {
203        Self {
204            test_req_id: Some(test_req_id),
205        }
206    }
207
208    /// Check if this heartbeat is a response to a test request
209    pub fn is_test_response(&self) -> bool {
210        self.test_req_id.is_some()
211    }
212
213    /// Build a FIX message for this Heartbeat
214    pub fn to_fix_message(
215        &self,
216        sender_comp_id: String,
217        target_comp_id: String,
218        msg_seq_num: u32,
219    ) -> Result<FixMessage> {
220        let mut builder = MessageBuilder::new()
221            .msg_type(MsgType::Heartbeat)
222            .sender_comp_id(sender_comp_id)
223            .target_comp_id(target_comp_id)
224            .msg_seq_num(msg_seq_num)
225            .sending_time(Utc::now());
226
227        // Add TestReqID if present
228        if let Some(ref test_req_id) = self.test_req_id {
229            builder = builder.field(112, test_req_id.clone());
230        }
231
232        builder.build()
233    }
234}
235
236impl TestRequest {
237    /// Create a new Test Request message with the specified ID
238    pub fn new(test_req_id: String) -> Self {
239        Self { test_req_id }
240    }
241
242    /// Generate a Test Request with a timestamp-based ID
243    pub fn new_with_timestamp() -> Self {
244        let test_req_id = format!("TESTREQ_{}", Utc::now().timestamp_millis());
245        Self::new(test_req_id)
246    }
247
248    /// Build a FIX message for this Test Request
249    pub fn to_fix_message(
250        &self,
251        sender_comp_id: String,
252        target_comp_id: String,
253        msg_seq_num: u32,
254    ) -> Result<FixMessage> {
255        MessageBuilder::new()
256            .msg_type(MsgType::TestRequest)
257            .sender_comp_id(sender_comp_id)
258            .target_comp_id(target_comp_id)
259            .msg_seq_num(msg_seq_num)
260            .sending_time(Utc::now())
261            .field(112, self.test_req_id.clone()) // TestReqID
262            .build()
263    }
264}
265
266impl ResendRequest {
267    /// Create a new Resend Request for a specific sequence range
268    pub fn new(begin_seq_no: u32, end_seq_no: u32) -> Self {
269        Self {
270            begin_seq_no,
271            end_seq_no,
272        }
273    }
274
275    /// Create a Resend Request for all messages from the specified sequence number
276    pub fn new_from_sequence(begin_seq_no: u32) -> Self {
277        Self {
278            begin_seq_no,
279            end_seq_no: 0, // 0 means "all messages from begin_seq_no"
280        }
281    }
282
283    /// Check if this request is for all messages from the begin sequence
284    pub fn is_infinite_range(&self) -> bool {
285        self.end_seq_no == 0
286    }
287
288    /// Get the number of messages requested (if not infinite range)
289    pub fn message_count(&self) -> Option<u32> {
290        if self.is_infinite_range() {
291            None
292        } else {
293            Some(self.end_seq_no.saturating_sub(self.begin_seq_no) + 1)
294        }
295    }
296
297    /// Build a FIX message for this Resend Request
298    pub fn to_fix_message(
299        &self,
300        sender_comp_id: String,
301        target_comp_id: String,
302        msg_seq_num: u32,
303    ) -> Result<FixMessage> {
304        MessageBuilder::new()
305            .msg_type(MsgType::ResendRequest)
306            .sender_comp_id(sender_comp_id)
307            .target_comp_id(target_comp_id)
308            .msg_seq_num(msg_seq_num)
309            .sending_time(Utc::now())
310            .field(7, self.begin_seq_no.to_string()) // BeginSeqNo
311            .field(16, self.end_seq_no.to_string()) // EndSeqNo
312            .build()
313    }
314}
315
316impl SequenceReset {
317    /// Create a new Sequence Reset message
318    pub fn new(new_seq_no: u32) -> Self {
319        Self {
320            new_seq_no,
321            gap_fill_flag: None,
322        }
323    }
324
325    /// Create a gap fill Sequence Reset message
326    pub fn new_gap_fill(new_seq_no: u32) -> Self {
327        Self {
328            new_seq_no,
329            gap_fill_flag: Some(true),
330        }
331    }
332
333    /// Create a sequence reset (not gap fill) message
334    pub fn new_reset(new_seq_no: u32) -> Self {
335        Self {
336            new_seq_no,
337            gap_fill_flag: Some(false),
338        }
339    }
340
341    /// Check if this is a gap fill message
342    pub fn is_gap_fill(&self) -> bool {
343        self.gap_fill_flag.unwrap_or(false)
344    }
345
346    /// Build a FIX message for this Sequence Reset
347    pub fn to_fix_message(
348        &self,
349        sender_comp_id: String,
350        target_comp_id: String,
351        msg_seq_num: u32,
352    ) -> Result<FixMessage> {
353        let mut builder = MessageBuilder::new()
354            .msg_type(MsgType::SequenceReset)
355            .sender_comp_id(sender_comp_id)
356            .target_comp_id(target_comp_id)
357            .msg_seq_num(msg_seq_num)
358            .sending_time(Utc::now())
359            .field(36, self.new_seq_no.to_string()); // NewSeqNo
360
361        // Add GapFillFlag if specified
362        if let Some(gap_fill) = self.gap_fill_flag {
363            builder = builder.field(123, if gap_fill { "Y" } else { "N" }.to_string());
364        }
365
366        builder.build()
367    }
368}
369
370impl Reject {
371    /// Create a new Reject message with minimal required fields
372    pub fn new(ref_seq_num: u32) -> Self {
373        Self {
374            ref_seq_num,
375            ref_tag_id: None,
376            ref_msg_type: None,
377            session_reject_reason: None,
378            text: None,
379        }
380    }
381
382    /// Create a Reject message with detailed rejection information
383    pub fn new_detailed(
384        ref_seq_num: u32,
385        ref_tag_id: Option<u32>,
386        ref_msg_type: Option<String>,
387        session_reject_reason: Option<SessionRejectReason>,
388        text: Option<String>,
389    ) -> Self {
390        Self {
391            ref_seq_num,
392            ref_tag_id,
393            ref_msg_type,
394            session_reject_reason: session_reject_reason.map(|r| r as u32),
395            text,
396        }
397    }
398
399    /// Create a Reject for invalid tag number
400    pub fn new_invalid_tag(ref_seq_num: u32, tag_id: u32) -> Self {
401        Self::new_detailed(
402            ref_seq_num,
403            Some(tag_id),
404            None,
405            Some(SessionRejectReason::InvalidTagNumber),
406            Some(format!("Invalid tag number: {tag_id}")),
407        )
408    }
409
410    /// Create a Reject for missing required tag
411    pub fn new_missing_tag(ref_seq_num: u32, tag_id: u32, msg_type: String) -> Self {
412        Self::new_detailed(
413            ref_seq_num,
414            Some(tag_id),
415            Some(msg_type),
416            Some(SessionRejectReason::RequiredTagMissing),
417            Some(format!("Required tag {tag_id} missing")),
418        )
419    }
420
421    /// Create a Reject for incorrect data format
422    pub fn new_incorrect_format(ref_seq_num: u32, tag_id: u32, text: String) -> Self {
423        Self::new_detailed(
424            ref_seq_num,
425            Some(tag_id),
426            None,
427            Some(SessionRejectReason::IncorrectDataFormat),
428            Some(text),
429        )
430    }
431
432    /// Build a FIX message for this Reject
433    pub fn to_fix_message(
434        &self,
435        sender_comp_id: String,
436        target_comp_id: String,
437        msg_seq_num: u32,
438    ) -> Result<FixMessage> {
439        let mut builder = MessageBuilder::new()
440            .msg_type(MsgType::Reject)
441            .sender_comp_id(sender_comp_id)
442            .target_comp_id(target_comp_id)
443            .msg_seq_num(msg_seq_num)
444            .sending_time(Utc::now())
445            .field(45, self.ref_seq_num.to_string()); // RefSeqNum
446
447        // Add optional fields
448        if let Some(ref_tag_id) = self.ref_tag_id {
449            builder = builder.field(371, ref_tag_id.to_string()); // RefTagID
450        }
451
452        if let Some(ref ref_msg_type) = self.ref_msg_type {
453            builder = builder.field(372, ref_msg_type.clone()); // RefMsgType
454        }
455
456        if let Some(session_reject_reason) = self.session_reject_reason {
457            builder = builder.field(373, session_reject_reason.to_string()); // SessionRejectReason
458        }
459
460        if let Some(ref text) = self.text {
461            builder = builder.field(58, text.clone()); // Text
462        }
463
464        builder.build()
465    }
466}
467
468impl BusinessMessageReject {
469    /// Create a new Business Message Reject
470    pub fn new(ref_msg_type: String, business_reject_reason: BusinessRejectReason) -> Self {
471        Self {
472            ref_msg_type,
473            business_reject_reason,
474            business_reject_ref_id: None,
475            text: None,
476        }
477    }
478
479    /// Set BusinessRejectRefID (379)
480    pub fn with_ref_id(mut self, business_reject_ref_id: String) -> Self {
481        self.business_reject_ref_id = Some(business_reject_ref_id);
482        self
483    }
484
485    /// Set Text (58)
486    pub fn with_text(mut self, text: String) -> Self {
487        self.text = Some(text);
488        self
489    }
490
491    /// Build a FIX message for this Business Message Reject
492    pub fn to_fix_message(
493        &self,
494        sender_comp_id: String,
495        target_comp_id: String,
496        msg_seq_num: u32,
497    ) -> Result<FixMessage> {
498        let mut builder = MessageBuilder::new()
499            .msg_type(MsgType::BusinessMessageReject)
500            .sender_comp_id(sender_comp_id)
501            .target_comp_id(target_comp_id)
502            .msg_seq_num(msg_seq_num)
503            .sending_time(Utc::now())
504            .field(372, self.ref_msg_type.clone()) // RefMsgType
505            .field(380, (self.business_reject_reason as u32).to_string()); // BusinessRejectReason
506
507        if let Some(ref ref_id) = self.business_reject_ref_id {
508            builder = builder.field(379, ref_id.clone()); // BusinessRejectRefID
509        }
510
511        if let Some(ref text) = self.text {
512            builder = builder.field(58, text.clone()); // Text
513        }
514
515        builder.build()
516    }
517}
518
519impl Default for Heartbeat {
520    fn default() -> Self {
521        Self::new()
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_heartbeat_creation() {
531        let heartbeat = Heartbeat::new();
532        assert_eq!(heartbeat.test_req_id, None);
533        assert!(!heartbeat.is_test_response());
534
535        let response = Heartbeat::new_response("TEST123".to_string());
536        assert_eq!(response.test_req_id, Some("TEST123".to_string()));
537        assert!(response.is_test_response());
538    }
539
540    #[test]
541    fn test_test_request_creation() {
542        let test_req = TestRequest::new("REQ123".to_string());
543        assert_eq!(test_req.test_req_id, "REQ123");
544
545        let timestamp_req = TestRequest::new_with_timestamp();
546        assert!(timestamp_req.test_req_id.starts_with("TESTREQ_"));
547    }
548
549    #[test]
550    fn test_resend_request_creation() {
551        let resend = ResendRequest::new(10, 20);
552        assert_eq!(resend.begin_seq_no, 10);
553        assert_eq!(resend.end_seq_no, 20);
554        assert!(!resend.is_infinite_range());
555        assert_eq!(resend.message_count(), Some(11));
556
557        let infinite = ResendRequest::new_from_sequence(15);
558        assert_eq!(infinite.begin_seq_no, 15);
559        assert_eq!(infinite.end_seq_no, 0);
560        assert!(infinite.is_infinite_range());
561        assert_eq!(infinite.message_count(), None);
562    }
563
564    #[test]
565    fn test_reject_creation() {
566        let basic_reject = Reject::new(123);
567        assert_eq!(basic_reject.ref_seq_num, 123);
568        assert_eq!(basic_reject.ref_tag_id, None);
569
570        let invalid_tag = Reject::new_invalid_tag(456, 999);
571        assert_eq!(invalid_tag.ref_seq_num, 456);
572        assert_eq!(invalid_tag.ref_tag_id, Some(999));
573        assert_eq!(
574            invalid_tag.session_reject_reason,
575            Some(SessionRejectReason::InvalidTagNumber as u32)
576        );
577
578        let missing_tag = Reject::new_missing_tag(789, 35, "D".to_string());
579        assert_eq!(missing_tag.ref_seq_num, 789);
580        assert_eq!(missing_tag.ref_tag_id, Some(35));
581        assert_eq!(missing_tag.ref_msg_type, Some("D".to_string()));
582        assert_eq!(
583            missing_tag.session_reject_reason,
584            Some(SessionRejectReason::RequiredTagMissing as u32)
585        );
586    }
587
588    #[test]
589    fn test_session_reject_reason_values() {
590        assert_eq!(SessionRejectReason::InvalidTagNumber as u32, 0);
591        assert_eq!(SessionRejectReason::RequiredTagMissing as u32, 1);
592        assert_eq!(SessionRejectReason::Other as u32, 99);
593    }
594
595    #[test]
596    fn test_heartbeat_to_fix_message() {
597        let heartbeat = Heartbeat::new_response("TEST123".to_string());
598        let fix_msg = heartbeat.to_fix_message("SENDER".to_string(), "TARGET".to_string(), 100);
599
600        assert!(fix_msg.is_ok());
601        let msg = fix_msg.unwrap();
602        assert_eq!(msg.get_field(35), Some(&"0".to_string())); // MsgType = Heartbeat
603        assert_eq!(msg.get_field(112), Some(&"TEST123".to_string())); // TestReqID
604    }
605
606    #[test]
607    fn test_test_request_to_fix_message() {
608        let test_req = TestRequest::new("REQ456".to_string());
609        let fix_msg = test_req.to_fix_message("CLIENT".to_string(), "SERVER".to_string(), 200);
610
611        assert!(fix_msg.is_ok());
612        let msg = fix_msg.unwrap();
613        assert_eq!(msg.get_field(35), Some(&"1".to_string())); // MsgType = TestRequest
614        assert_eq!(msg.get_field(112), Some(&"REQ456".to_string())); // TestReqID
615    }
616
617    #[test]
618    fn test_business_message_reject_to_fix_message() {
619        let bmr = BusinessMessageReject::new(
620            "D".to_string(),
621            BusinessRejectReason::UnsupportedMessageType,
622        )
623        .with_ref_id("ABC123".to_string())
624        .with_text("Unsupported type".to_string());
625
626        let fix_msg = bmr.to_fix_message("SENDER".to_string(), "TARGET".to_string(), 77);
627        assert!(fix_msg.is_ok());
628        let msg = fix_msg.unwrap();
629        assert_eq!(msg.get_field(35), Some(&"j".to_string())); // MsgType = BusinessMessageReject
630        assert_eq!(msg.get_field(372), Some(&"D".to_string())); // RefMsgType
631        assert_eq!(msg.get_field(379), Some(&"ABC123".to_string())); // BusinessRejectRefID
632        assert_eq!(
633            msg.get_field(380),
634            Some(&(BusinessRejectReason::UnsupportedMessageType as u32).to_string())
635        ); // Reason
636        assert_eq!(msg.get_field(58), Some(&"Unsupported type".to_string())); // Text
637    }
638}