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