deribit_fix/message/
security_definition.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 14/8/25
5******************************************************************************/
6
7//! FIX Security Definition messages implementation
8//!
9//! This module provides functionality for creating and parsing FIX security
10//! definition messages used in communication with Deribit, including:
11//! - SecurityDefinitionRequest (MsgType = "c")
12//! - SecurityDefinition (MsgType = "d")
13
14use crate::error::{DeribitFixError, Result as DeribitFixResult};
15use crate::message::MessageBuilder;
16use crate::model::message::FixMessage;
17use crate::model::types::MsgType;
18use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21/// Security Definition Request Type enumeration (tag 856)
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum SecurityDefinitionRequestType {
24    /// Request Security identity and specifications (0)
25    RequestSecurityIdentityAndSpecs,
26    /// Request Security identity for the specifications provided (1)
27    RequestSecurityIdentityForSpecs,
28    /// Request List Security Types (2)
29    RequestListSecurityTypes,
30    /// Request List Securities (3)
31    RequestListSecurities,
32}
33
34impl From<SecurityDefinitionRequestType> for i32 {
35    fn from(request_type: SecurityDefinitionRequestType) -> Self {
36        match request_type {
37            SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs => 0,
38            SecurityDefinitionRequestType::RequestSecurityIdentityForSpecs => 1,
39            SecurityDefinitionRequestType::RequestListSecurityTypes => 2,
40            SecurityDefinitionRequestType::RequestListSecurities => 3,
41        }
42    }
43}
44
45impl TryFrom<i32> for SecurityDefinitionRequestType {
46    type Error = DeribitFixError;
47
48    fn try_from(value: i32) -> Result<Self, Self::Error> {
49        match value {
50            0 => Ok(SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs),
51            1 => Ok(SecurityDefinitionRequestType::RequestSecurityIdentityForSpecs),
52            2 => Ok(SecurityDefinitionRequestType::RequestListSecurityTypes),
53            3 => Ok(SecurityDefinitionRequestType::RequestListSecurities),
54            _ => Err(DeribitFixError::MessageParsing(format!(
55                "Invalid SecurityDefinitionRequestType: {}",
56                value
57            ))),
58        }
59    }
60}
61
62/// Security Definition Request message (MsgType = "c")
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SecurityDefinitionRequest {
65    /// Security Request ID (tag 320)
66    pub security_req_id: String,
67    /// Security Definition Request Type (tag 856)
68    pub request_type: SecurityDefinitionRequestType,
69    /// Symbol (tag 55) - optional
70    pub symbol: Option<String>,
71    /// Security Type (tag 167) - optional
72    pub security_type: Option<String>,
73    /// Currency (tag 15) - optional
74    pub currency: Option<String>,
75    /// Text (tag 58) - optional
76    pub text: Option<String>,
77    /// Subscription Request Type (tag 263) - optional
78    pub subscription_request_type: Option<i32>,
79}
80
81impl SecurityDefinitionRequest {
82    /// Create a new Security Definition Request
83    pub fn new(security_req_id: String, request_type: SecurityDefinitionRequestType) -> Self {
84        Self {
85            security_req_id,
86            request_type,
87            symbol: None,
88            security_type: None,
89            currency: None,
90            text: None,
91            subscription_request_type: None,
92        }
93    }
94
95    /// Create a request for security identity and specifications
96    pub fn request_identity_and_specs(security_req_id: String) -> Self {
97        Self::new(
98            security_req_id,
99            SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs,
100        )
101    }
102
103    /// Create a request for listing securities
104    pub fn request_list_securities(security_req_id: String) -> Self {
105        Self::new(
106            security_req_id,
107            SecurityDefinitionRequestType::RequestListSecurities,
108        )
109    }
110
111    /// Add symbol filter
112    pub fn with_symbol(mut self, symbol: String) -> Self {
113        self.symbol = Some(symbol);
114        self
115    }
116
117    /// Add security type filter
118    pub fn with_security_type(mut self, security_type: String) -> Self {
119        self.security_type = Some(security_type);
120        self
121    }
122
123    /// Add currency filter
124    pub fn with_currency(mut self, currency: String) -> Self {
125        self.currency = Some(currency);
126        self
127    }
128
129    /// Add text description
130    pub fn with_text(mut self, text: String) -> Self {
131        self.text = Some(text);
132        self
133    }
134
135    /// Add subscription request type
136    pub fn with_subscription_request_type(mut self, subscription_type: i32) -> Self {
137        self.subscription_request_type = Some(subscription_type);
138        self
139    }
140
141    /// Convert to FIX message
142    pub fn to_fix_message(
143        &self,
144        sender_comp_id: String,
145        target_comp_id: String,
146        msg_seq_num: u32,
147    ) -> DeribitFixResult<FixMessage> {
148        let mut builder = MessageBuilder::new()
149            .msg_type(MsgType::SecurityDefinitionRequest)
150            .sender_comp_id(sender_comp_id)
151            .target_comp_id(target_comp_id)
152            .msg_seq_num(msg_seq_num)
153            .field(320, self.security_req_id.clone()) // SecurityReqID
154            .field(856, i32::from(self.request_type).to_string()); // SecurityDefinitionRequestType
155
156        // Add optional fields
157        if let Some(ref symbol) = self.symbol {
158            builder = builder.field(55, symbol.clone()); // Symbol
159        }
160
161        if let Some(ref security_type) = self.security_type {
162            builder = builder.field(167, security_type.clone()); // SecurityType
163        }
164
165        if let Some(ref currency) = self.currency {
166            builder = builder.field(15, currency.clone()); // Currency
167        }
168
169        if let Some(ref text) = self.text {
170            builder = builder.field(58, text.clone()); // Text
171        }
172
173        if let Some(subscription_type) = self.subscription_request_type {
174            builder = builder.field(263, subscription_type.to_string()); // SubscriptionRequestType
175        }
176
177        builder.build()
178    }
179}
180
181/// Security Definition message (MsgType = "d")
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct SecurityDefinition {
184    /// Security Request ID (tag 320)
185    pub security_req_id: String,
186    /// Security Response ID (tag 322)
187    pub security_response_id: String,
188    /// Symbol (tag 55)
189    pub symbol: String,
190    /// Security Type (tag 167) - optional
191    pub security_type: Option<String>,
192    /// Currency (tag 15) - optional
193    pub currency: Option<String>,
194    /// Security Description (tag 107) - optional
195    pub security_desc: Option<String>,
196    /// Strike Price (tag 202) - optional for options
197    pub strike_price: Option<f64>,
198    /// Strike Currency (tag 947) - optional for options
199    pub strike_currency: Option<String>,
200    /// Put or Call (tag 201) - optional for options (0=Put, 1=Call)
201    pub put_or_call: Option<i32>,
202    /// Contract Multiplier (tag 231) - optional
203    pub contract_multiplier: Option<f64>,
204    /// Maturity Date (tag 541) - optional
205    pub maturity_date: Option<String>,
206    /// Issue Date (tag 225) - optional
207    pub issue_date: Option<String>,
208    /// Minimum Trade Volume (tag 562) - optional
209    pub min_trade_vol: Option<f64>,
210    /// Security Definition Response Type (tag 1570) - optional
211    pub security_def_response_type: Option<i32>,
212    /// Last update time
213    pub last_update_time: Option<DateTime<Utc>>,
214}
215
216impl SecurityDefinition {
217    /// Create a new Security Definition
218    pub fn new(security_req_id: String, security_response_id: String, symbol: String) -> Self {
219        Self {
220            security_req_id,
221            security_response_id,
222            symbol,
223            security_type: None,
224            currency: None,
225            security_desc: None,
226            strike_price: None,
227            strike_currency: None,
228            put_or_call: None,
229            contract_multiplier: None,
230            maturity_date: None,
231            issue_date: None,
232            min_trade_vol: None,
233            security_def_response_type: None,
234            last_update_time: Some(Utc::now()),
235        }
236    }
237
238    /// Parse from FIX message
239    pub fn from_fix_message(message: &FixMessage) -> DeribitFixResult<Self> {
240        let security_req_id = message
241            .get_field(320)
242            .ok_or_else(|| {
243                DeribitFixError::MessageParsing("Missing SecurityReqID (320)".to_string())
244            })?
245            .clone();
246
247        let security_response_id = message
248            .get_field(322)
249            .ok_or_else(|| {
250                DeribitFixError::MessageParsing("Missing SecurityResponseID (322)".to_string())
251            })?
252            .clone();
253
254        let symbol = message
255            .get_field(55)
256            .ok_or_else(|| DeribitFixError::MessageParsing("Missing Symbol (55)".to_string()))?
257            .clone();
258
259        let security_type = message.get_field(167).cloned();
260        let currency = message.get_field(15).cloned();
261        let security_desc = message.get_field(107).cloned();
262
263        let strike_price = message.get_field(202).and_then(|s| s.parse::<f64>().ok());
264
265        let strike_currency = message.get_field(947).cloned();
266
267        let put_or_call = message.get_field(201).and_then(|s| s.parse::<i32>().ok());
268
269        let contract_multiplier = message.get_field(231).and_then(|s| s.parse::<f64>().ok());
270
271        let maturity_date = message.get_field(541).cloned();
272        let issue_date = message.get_field(225).cloned();
273
274        let min_trade_vol = message.get_field(562).and_then(|s| s.parse::<f64>().ok());
275
276        let security_def_response_type =
277            message.get_field(1570).and_then(|s| s.parse::<i32>().ok());
278
279        Ok(Self {
280            security_req_id,
281            security_response_id,
282            symbol,
283            security_type,
284            currency,
285            security_desc,
286            strike_price,
287            strike_currency,
288            put_or_call,
289            contract_multiplier,
290            maturity_date,
291            issue_date,
292            min_trade_vol,
293            security_def_response_type,
294            last_update_time: Some(Utc::now()),
295        })
296    }
297
298    /// Convert to FIX message
299    pub fn to_fix_message(
300        &self,
301        sender_comp_id: String,
302        target_comp_id: String,
303        msg_seq_num: u32,
304    ) -> DeribitFixResult<FixMessage> {
305        let mut builder = MessageBuilder::new()
306            .msg_type(MsgType::SecurityDefinition)
307            .sender_comp_id(sender_comp_id)
308            .target_comp_id(target_comp_id)
309            .msg_seq_num(msg_seq_num)
310            .field(320, self.security_req_id.clone()) // SecurityReqID
311            .field(322, self.security_response_id.clone()) // SecurityResponseID
312            .field(55, self.symbol.clone()); // Symbol
313
314        // Add optional fields
315        if let Some(ref security_type) = self.security_type {
316            builder = builder.field(167, security_type.clone()); // SecurityType
317        }
318
319        if let Some(ref currency) = self.currency {
320            builder = builder.field(15, currency.clone()); // Currency
321        }
322
323        if let Some(ref security_desc) = self.security_desc {
324            builder = builder.field(107, security_desc.clone()); // SecurityDesc
325        }
326
327        if let Some(strike_price) = self.strike_price {
328            builder = builder.field(202, strike_price.to_string()); // StrikePrice
329        }
330
331        if let Some(ref strike_currency) = self.strike_currency {
332            builder = builder.field(947, strike_currency.clone()); // StrikeCurrency
333        }
334
335        if let Some(put_or_call) = self.put_or_call {
336            builder = builder.field(201, put_or_call.to_string()); // PutOrCall
337        }
338
339        if let Some(contract_multiplier) = self.contract_multiplier {
340            builder = builder.field(231, contract_multiplier.to_string()); // ContractMultiplier
341        }
342
343        if let Some(ref maturity_date) = self.maturity_date {
344            builder = builder.field(541, maturity_date.clone()); // MaturityDate
345        }
346
347        if let Some(ref issue_date) = self.issue_date {
348            builder = builder.field(225, issue_date.clone()); // IssueDate
349        }
350
351        if let Some(min_trade_vol) = self.min_trade_vol {
352            builder = builder.field(562, min_trade_vol.to_string()); // MinTradeVol
353        }
354
355        if let Some(response_type) = self.security_def_response_type {
356            builder = builder.field(1570, response_type.to_string()); // SecurityDefinitionResponseType
357        }
358
359        builder.build()
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_security_definition_request_type_conversion() {
369        assert_eq!(
370            i32::from(SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs),
371            0
372        );
373        assert_eq!(
374            i32::from(SecurityDefinitionRequestType::RequestSecurityIdentityForSpecs),
375            1
376        );
377
378        assert_eq!(
379            SecurityDefinitionRequestType::try_from(0).unwrap(),
380            SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs
381        );
382        assert_eq!(
383            SecurityDefinitionRequestType::try_from(1).unwrap(),
384            SecurityDefinitionRequestType::RequestSecurityIdentityForSpecs
385        );
386
387        assert!(SecurityDefinitionRequestType::try_from(99).is_err());
388    }
389
390    #[test]
391    fn test_security_definition_request_creation() {
392        let request =
393            SecurityDefinitionRequest::request_identity_and_specs("SECDEF_123".to_string());
394        assert_eq!(request.security_req_id, "SECDEF_123");
395        assert_eq!(
396            request.request_type,
397            SecurityDefinitionRequestType::RequestSecurityIdentityAndSpecs
398        );
399    }
400
401    #[test]
402    fn test_security_definition_request_with_filters() {
403        let request = SecurityDefinitionRequest::request_list_securities("SECDEF_456".to_string())
404            .with_symbol("BTC-PERPETUAL".to_string())
405            .with_currency("USD".to_string())
406            .with_security_type("FUT".to_string());
407
408        assert_eq!(request.security_req_id, "SECDEF_456");
409        assert_eq!(request.symbol, Some("BTC-PERPETUAL".to_string()));
410        assert_eq!(request.currency, Some("USD".to_string()));
411        assert_eq!(request.security_type, Some("FUT".to_string()));
412    }
413
414    #[test]
415    fn test_security_definition_request_to_fix_message() {
416        let request =
417            SecurityDefinitionRequest::request_identity_and_specs("SECDEF_789".to_string())
418                .with_symbol("ETH-PERPETUAL".to_string());
419
420        let fix_message = request
421            .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
422            .unwrap();
423
424        assert_eq!(fix_message.get_field(35), Some(&"c".to_string())); // MsgType
425        assert_eq!(fix_message.get_field(320), Some(&"SECDEF_789".to_string())); // SecurityReqID
426        assert_eq!(fix_message.get_field(856), Some(&"0".to_string())); // SecurityDefinitionRequestType
427        assert_eq!(
428            fix_message.get_field(55),
429            Some(&"ETH-PERPETUAL".to_string())
430        ); // Symbol
431    }
432
433    #[test]
434    fn test_security_definition_creation() {
435        let definition = SecurityDefinition::new(
436            "SECDEF_123".to_string(),
437            "RESP_456".to_string(),
438            "BTC-PERPETUAL".to_string(),
439        );
440
441        assert_eq!(definition.security_req_id, "SECDEF_123");
442        assert_eq!(definition.security_response_id, "RESP_456");
443        assert_eq!(definition.symbol, "BTC-PERPETUAL");
444        assert!(definition.last_update_time.is_some());
445    }
446
447    #[test]
448    fn test_security_definition_from_fix_message() {
449        let mut fix_message = FixMessage::new();
450        fix_message.set_field(320, "SECDEF_123".to_string());
451        fix_message.set_field(322, "RESP_456".to_string());
452        fix_message.set_field(55, "BTC-PERPETUAL".to_string());
453        fix_message.set_field(167, "FUT".to_string());
454        fix_message.set_field(15, "USD".to_string());
455        fix_message.set_field(107, "Bitcoin Perpetual Future".to_string());
456
457        let definition = SecurityDefinition::from_fix_message(&fix_message).unwrap();
458
459        assert_eq!(definition.security_req_id, "SECDEF_123");
460        assert_eq!(definition.security_response_id, "RESP_456");
461        assert_eq!(definition.symbol, "BTC-PERPETUAL");
462        assert_eq!(definition.security_type, Some("FUT".to_string()));
463        assert_eq!(definition.currency, Some("USD".to_string()));
464        assert_eq!(
465            definition.security_desc,
466            Some("Bitcoin Perpetual Future".to_string())
467        );
468    }
469
470    #[test]
471    fn test_security_definition_to_fix_message() {
472        let definition = SecurityDefinition::new(
473            "SECDEF_123".to_string(),
474            "RESP_456".to_string(),
475            "BTC-PERPETUAL".to_string(),
476        );
477
478        let fix_message = definition
479            .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
480            .unwrap();
481
482        assert_eq!(fix_message.get_field(35), Some(&"d".to_string())); // MsgType
483        assert_eq!(fix_message.get_field(320), Some(&"SECDEF_123".to_string())); // SecurityReqID
484        assert_eq!(fix_message.get_field(322), Some(&"RESP_456".to_string())); // SecurityResponseID
485        assert_eq!(
486            fix_message.get_field(55),
487            Some(&"BTC-PERPETUAL".to_string())
488        ); // Symbol
489    }
490}