deribit_fix/message/
security_list.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 22/7/25
5******************************************************************************/
6
7//! Security List Request and Security List messages for FIX protocol
8//!
9//! This module implements the FIX messages for requesting and receiving
10//! security/instrument information from Deribit according to the official
11//! FIX API specification.
12
13use crate::error::Result as DeribitFixResult;
14use crate::message::MessageBuilder;
15use crate::model::types::MsgType;
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18
19/// Security List Request Type enumeration
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum SecurityListRequestType {
22    /// Snapshot - return current list of instruments
23    Snapshot = 0,
24    /// Snapshot + Updates - return current list and subscribe to updates
25    SnapshotAndUpdates = 4,
26}
27
28impl From<SecurityListRequestType> for i32 {
29    fn from(request_type: SecurityListRequestType) -> Self {
30        request_type as i32
31    }
32}
33
34impl TryFrom<i32> for SecurityListRequestType {
35    type Error = String;
36
37    fn try_from(value: i32) -> Result<Self, Self::Error> {
38        match value {
39            0 => Ok(SecurityListRequestType::Snapshot),
40            4 => Ok(SecurityListRequestType::SnapshotAndUpdates),
41            _ => Err(format!("Invalid SecurityListRequestType: {value}")),
42        }
43    }
44}
45
46/// Subscription Request Type for security list updates
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub enum SubscriptionRequestType {
49    /// Snapshot only
50    Snapshot = 0,
51    /// Snapshot + Updates (Subscribe)
52    SnapshotPlusUpdates = 1,
53    /// Disable previous Snapshot + Update Request (Unsubscribe)
54    Unsubscribe = 2,
55}
56
57impl From<SubscriptionRequestType> for i32 {
58    fn from(request_type: SubscriptionRequestType) -> Self {
59        request_type as i32
60    }
61}
62
63impl TryFrom<i32> for SubscriptionRequestType {
64    type Error = String;
65
66    fn try_from(value: i32) -> Result<Self, Self::Error> {
67        match value {
68            0 => Ok(SubscriptionRequestType::Snapshot),
69            1 => Ok(SubscriptionRequestType::SnapshotPlusUpdates),
70            2 => Ok(SubscriptionRequestType::Unsubscribe),
71            _ => Err(format!("Invalid SubscriptionRequestType: {value}")),
72        }
73    }
74}
75
76/// Security Type enumeration for FIX protocol
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum SecurityType {
79    /// Currency exchange spot
80    FxSpot,
81    /// Futures
82    Future,
83    /// Options
84    Option,
85    /// Future combo
86    FutureCombo,
87    /// Option combo
88    OptionCombo,
89    /// Indexes
90    Index,
91}
92
93impl SecurityType {
94    /// Convert to FIX string representation
95    pub fn as_fix_str(&self) -> &'static str {
96        match self {
97            SecurityType::FxSpot => "FXSPOT",
98            SecurityType::Future => "FUT",
99            SecurityType::Option => "OPT",
100            SecurityType::FutureCombo => "FUTCO",
101            SecurityType::OptionCombo => "OPTCO",
102            SecurityType::Index => "INDEX",
103        }
104    }
105
106    /// Parse from FIX string representation
107    pub fn from_fix_str(s: &str) -> Result<Self, String> {
108        match s {
109            "FXSPOT" => Ok(SecurityType::FxSpot),
110            "FUT" => Ok(SecurityType::Future),
111            "OPT" => Ok(SecurityType::Option),
112            "FUTCO" => Ok(SecurityType::FutureCombo),
113            "OPTCO" => Ok(SecurityType::OptionCombo),
114            "INDEX" => Ok(SecurityType::Index),
115            _ => Err(format!("Invalid SecurityType: {s}")),
116        }
117    }
118}
119
120/// Security List Request message (MsgType = x)
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct SecurityListRequest {
123    /// User-generated ID for this request (Tag 320)
124    pub security_req_id: String,
125    /// Security List Request Type (Tag 559)
126    pub security_list_request_type: SecurityListRequestType,
127    /// Subscription Request Type (Tag 263) - Optional
128    pub subscription_request_type: Option<SubscriptionRequestType>,
129    /// Display Multicast Instrument ID (Tag 9013) - Custom tag
130    pub display_multicast_instrument_id: Option<bool>,
131    /// Display Increment Steps (Tag 9018) - Custom tag
132    pub display_increment_steps: Option<bool>,
133    /// Currency filter (Tag 15) - Optional
134    pub currency: Option<String>,
135    /// Secondary Currency filter (Tag 5544) - Optional
136    pub secondary_currency: Option<String>,
137    /// Security Type filter (Tag 167) - Optional
138    pub security_type: Option<SecurityType>,
139}
140
141impl SecurityListRequest {
142    /// Create a new Security List Request
143    pub fn new(security_req_id: String, request_type: SecurityListRequestType) -> Self {
144        Self {
145            security_req_id,
146            security_list_request_type: request_type,
147            subscription_request_type: None,
148            display_multicast_instrument_id: None,
149            display_increment_steps: None,
150            currency: None,
151            secondary_currency: None,
152            security_type: None,
153        }
154    }
155
156    /// Create a snapshot request for all instruments
157    pub fn snapshot(security_req_id: String) -> Self {
158        Self::new(security_req_id, SecurityListRequestType::Snapshot)
159    }
160
161    /// Create a subscription request for instrument updates
162    pub fn subscription(security_req_id: String) -> Self {
163        let mut request = Self::new(security_req_id, SecurityListRequestType::SnapshotAndUpdates);
164        request.subscription_request_type = Some(SubscriptionRequestType::SnapshotPlusUpdates);
165        request
166    }
167
168    /// Set currency filter
169    pub fn with_currency(mut self, currency: String) -> Self {
170        self.currency = Some(currency);
171        self
172    }
173
174    /// Set secondary currency filter
175    pub fn with_secondary_currency(mut self, secondary_currency: String) -> Self {
176        self.secondary_currency = Some(secondary_currency);
177        self
178    }
179
180    /// Set security type filter
181    pub fn with_security_type(mut self, security_type: SecurityType) -> Self {
182        self.security_type = Some(security_type);
183        self
184    }
185
186    /// Enable multicast instrument ID display
187    pub fn with_multicast_instrument_id(mut self, enable: bool) -> Self {
188        self.display_multicast_instrument_id = Some(enable);
189        self
190    }
191
192    /// Enable increment steps display
193    pub fn with_increment_steps(mut self, enable: bool) -> Self {
194        self.display_increment_steps = Some(enable);
195        self
196    }
197
198    /// Convert to FIX message
199    pub fn to_fix_message(
200        &self,
201        sender_comp_id: String,
202        target_comp_id: String,
203        msg_seq_num: u32,
204    ) -> DeribitFixResult<crate::model::message::FixMessage> {
205        let mut builder = MessageBuilder::new()
206            .msg_type(MsgType::SecurityListRequest)
207            .sender_comp_id(sender_comp_id)
208            .target_comp_id(target_comp_id)
209            .msg_seq_num(msg_seq_num)
210            .sending_time(Utc::now())
211            .field(320, self.security_req_id.clone()) // SecurityReqId
212            .field(559, i32::from(self.security_list_request_type).to_string()); // SecurityListRequestType
213
214        // Add optional fields
215        if let Some(subscription_type) = self.subscription_request_type {
216            builder = builder.field(263, i32::from(subscription_type).to_string()); // SubscriptionRequestType
217        }
218
219        if let Some(display_multicast) = self.display_multicast_instrument_id {
220            builder = builder.field(
221                9013,
222                if display_multicast {
223                    "Y".to_string()
224                } else {
225                    "N".to_string()
226                },
227            ); // DisplayMulticastInstrumentID
228        }
229
230        if let Some(display_steps) = self.display_increment_steps {
231            builder = builder.field(
232                9018,
233                if display_steps {
234                    "Y".to_string()
235                } else {
236                    "N".to_string()
237                },
238            ); // DisplayIncrementSteps
239        }
240
241        if let Some(ref currency) = self.currency {
242            builder = builder.field(15, currency.clone()); // Currency
243        }
244
245        if let Some(ref secondary_currency) = self.secondary_currency {
246            builder = builder.field(5544, secondary_currency.clone()); // SecondaryCurrency
247        }
248
249        if let Some(ref security_type) = self.security_type {
250            builder = builder.field(167, security_type.as_fix_str().to_string()); // SecurityType
251        }
252
253        builder.build()
254    }
255}
256
257/// Put or Call indicator for options
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259pub enum PutOrCall {
260    /// Put option
261    Put = 0,
262    /// Call option
263    Call = 1,
264}
265
266impl From<PutOrCall> for i32 {
267    fn from(put_or_call: PutOrCall) -> Self {
268        put_or_call as i32
269    }
270}
271
272impl TryFrom<i32> for PutOrCall {
273    type Error = String;
274
275    fn try_from(value: i32) -> Result<Self, Self::Error> {
276        match value {
277            0 => Ok(PutOrCall::Put),
278            1 => Ok(PutOrCall::Call),
279            _ => Err(format!("Invalid PutOrCall: {value}")),
280        }
281    }
282}
283
284/// Security Status enumeration
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
286pub enum SecurityStatus {
287    /// Active/Started
288    Active = 1,
289    /// Terminated/Inactive
290    Terminated = 2,
291    /// Closed
292    Closed = 4,
293    /// Published/Created
294    Published = 10,
295    /// Settled
296    Settled = 12,
297}
298
299impl From<SecurityStatus> for i32 {
300    fn from(status: SecurityStatus) -> Self {
301        status as i32
302    }
303}
304
305impl TryFrom<i32> for SecurityStatus {
306    type Error = String;
307
308    fn try_from(value: i32) -> Result<Self, Self::Error> {
309        match value {
310            1 => Ok(SecurityStatus::Active),
311            2 => Ok(SecurityStatus::Terminated),
312            4 => Ok(SecurityStatus::Closed),
313            10 => Ok(SecurityStatus::Published),
314            12 => Ok(SecurityStatus::Settled),
315            _ => Err(format!("Invalid SecurityStatus: {value}")),
316        }
317    }
318}
319
320/// Security Alternative ID information
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct SecurityAltId {
323    /// Security identifier (Tag 455)
324    pub security_alt_id: String,
325    /// Source of the identifier (Tag 456)
326    pub security_alt_id_source: String,
327}
328
329impl SecurityAltId {
330    /// Create multicast identifier
331    pub fn multicast(id: String) -> Self {
332        Self {
333            security_alt_id: id,
334            security_alt_id_source: "101".to_string(), // Multicast identifier
335        }
336    }
337
338    /// Create combo instrument identifier
339    pub fn combo(id: String) -> Self {
340        Self {
341            security_alt_id: id,
342            security_alt_id_source: "102".to_string(), // Combo instrument identifier
343        }
344    }
345}
346
347/// Price increment rule
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct TickRule {
350    /// Above this price, the tick increment applies (Tag 1206)
351    pub start_tick_price_range: f64,
352    /// Valid price increment for prices above the start range (Tag 1208)
353    pub tick_increment: f64,
354}
355
356/// Security information in Security List response
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct SecurityInfo {
359    /// Symbol name (Tag 55)
360    pub symbol: String,
361    /// Security description (Tag 107)
362    pub security_desc: Option<String>,
363    /// Security type (Tag 167)
364    pub security_type: Option<SecurityType>,
365    /// Put or Call indicator (Tag 201) - Options only
366    pub put_or_call: Option<PutOrCall>,
367    /// Strike price (Tag 202) - Options only
368    pub strike_price: Option<f64>,
369    /// Strike currency (Tag 947)
370    pub strike_currency: Option<String>,
371    /// Currency (Tag 15)
372    pub currency: Option<String>,
373    /// Price quote currency (Tag 1524)
374    pub price_quote_currency: Option<String>,
375    /// Instrument price precision (Tag 2576)
376    pub instrument_price_precision: Option<i32>,
377    /// Minimum price increment (Tag 969)
378    pub min_price_increment: Option<f64>,
379    /// Underlying symbol (Tag 311) - Options only
380    pub underlying_symbol: Option<String>,
381    /// Issue date (Tag 225)
382    pub issue_date: Option<DateTime<Utc>>,
383    /// Maturity date (Tag 541)
384    pub maturity_date: Option<DateTime<Utc>>,
385    /// Maturity time (Tag 1079)
386    pub maturity_time: Option<DateTime<Utc>>,
387    /// Minimum trade volume (Tag 562)
388    pub min_trade_vol: Option<f64>,
389    /// Settlement type (Tag 63)
390    pub settl_type: Option<String>,
391    /// Settlement currency (Tag 120)
392    pub settl_currency: Option<String>,
393    /// Commission currency (Tag 479)
394    pub comm_currency: Option<String>,
395    /// Contract multiplier (Tag 231)
396    pub contract_multiplier: Option<f64>,
397    /// Alternative security identifiers (Tag 454)
398    pub security_alt_ids: Vec<SecurityAltId>,
399    /// Price increment rules (Tag 1205)
400    pub tick_rules: Vec<TickRule>,
401    /// Security status (Tag 965) - Present in notifications
402    pub security_status: Option<SecurityStatus>,
403}
404
405impl SecurityInfo {
406    /// Create a new security info with minimal required fields
407    pub fn new(symbol: String) -> Self {
408        Self {
409            symbol,
410            security_desc: None,
411            security_type: None,
412            put_or_call: None,
413            strike_price: None,
414            strike_currency: None,
415            currency: None,
416            price_quote_currency: None,
417            instrument_price_precision: None,
418            min_price_increment: None,
419            underlying_symbol: None,
420            issue_date: None,
421            maturity_date: None,
422            maturity_time: None,
423            min_trade_vol: None,
424            settl_type: None,
425            settl_currency: None,
426            comm_currency: None,
427            contract_multiplier: None,
428            security_alt_ids: Vec::new(),
429            tick_rules: Vec::new(),
430            security_status: None,
431        }
432    }
433
434    /// Check if this is an option
435    pub fn is_option(&self) -> bool {
436        matches!(
437            self.security_type,
438            Some(SecurityType::Option | SecurityType::OptionCombo)
439        )
440    }
441
442    /// Check if this is a future
443    pub fn is_future(&self) -> bool {
444        matches!(
445            self.security_type,
446            Some(SecurityType::Future | SecurityType::FutureCombo)
447        )
448    }
449
450    /// Check if this is a spot instrument
451    pub fn is_spot(&self) -> bool {
452        matches!(self.security_type, Some(SecurityType::FxSpot))
453    }
454
455    /// Set security description
456    pub fn with_security_desc(mut self, desc: String) -> Self {
457        self.security_desc = Some(desc);
458        self
459    }
460
461    /// Set security type
462    pub fn with_security_type(mut self, security_type: SecurityType) -> Self {
463        self.security_type = Some(security_type);
464        self
465    }
466
467    /// Set put or call for options
468    pub fn with_put_or_call(mut self, put_or_call: PutOrCall) -> Self {
469        self.put_or_call = Some(put_or_call);
470        self
471    }
472
473    /// Set strike price for options
474    pub fn with_strike_price(mut self, strike_price: f64) -> Self {
475        self.strike_price = Some(strike_price);
476        self
477    }
478
479    /// Set strike currency
480    pub fn with_strike_currency(mut self, strike_currency: String) -> Self {
481        self.strike_currency = Some(strike_currency);
482        self
483    }
484
485    /// Set currency
486    pub fn with_currency(mut self, currency: String) -> Self {
487        self.currency = Some(currency);
488        self
489    }
490
491    /// Set price quote currency
492    pub fn with_price_quote_currency(mut self, currency: String) -> Self {
493        self.price_quote_currency = Some(currency);
494        self
495    }
496
497    /// Set instrument price precision
498    pub fn with_instrument_price_precision(mut self, precision: i32) -> Self {
499        self.instrument_price_precision = Some(precision);
500        self
501    }
502
503    /// Set minimum price increment
504    pub fn with_min_price_increment(mut self, increment: f64) -> Self {
505        self.min_price_increment = Some(increment);
506        self
507    }
508
509    /// Set underlying symbol for options
510    pub fn with_underlying_symbol(mut self, underlying: String) -> Self {
511        self.underlying_symbol = Some(underlying);
512        self
513    }
514
515    /// Set issue date
516    pub fn with_issue_date(mut self, issue_date: DateTime<Utc>) -> Self {
517        self.issue_date = Some(issue_date);
518        self
519    }
520
521    /// Set maturity date
522    pub fn with_maturity_date(mut self, maturity_date: DateTime<Utc>) -> Self {
523        self.maturity_date = Some(maturity_date);
524        self
525    }
526
527    /// Set maturity time
528    pub fn with_maturity_time(mut self, maturity_time: DateTime<Utc>) -> Self {
529        self.maturity_time = Some(maturity_time);
530        self
531    }
532
533    /// Set minimum trade volume
534    pub fn with_min_trade_vol(mut self, min_vol: f64) -> Self {
535        self.min_trade_vol = Some(min_vol);
536        self
537    }
538
539    /// Set settlement type
540    pub fn with_settl_type(mut self, settl_type: String) -> Self {
541        self.settl_type = Some(settl_type);
542        self
543    }
544
545    /// Set settlement currency
546    pub fn with_settl_currency(mut self, currency: String) -> Self {
547        self.settl_currency = Some(currency);
548        self
549    }
550
551    /// Set commission currency
552    pub fn with_comm_currency(mut self, currency: String) -> Self {
553        self.comm_currency = Some(currency);
554        self
555    }
556
557    /// Set contract multiplier
558    pub fn with_contract_multiplier(mut self, multiplier: f64) -> Self {
559        self.contract_multiplier = Some(multiplier);
560        self
561    }
562
563    /// Add a security alternative ID
564    pub fn add_security_alt_id(mut self, alt_id: SecurityAltId) -> Self {
565        self.security_alt_ids.push(alt_id);
566        self
567    }
568
569    /// Add a tick rule
570    pub fn add_tick_rule(mut self, tick_rule: TickRule) -> Self {
571        self.tick_rules.push(tick_rule);
572        self
573    }
574
575    /// Set security status
576    pub fn with_security_status(mut self, status: SecurityStatus) -> Self {
577        self.security_status = Some(status);
578        self
579    }
580}
581
582/// Security List response message (MsgType = y)
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct SecurityList {
585    /// Security Request ID from the original request (Tag 320)
586    pub security_req_id: String,
587    /// Security Response ID (Tag 322)
588    pub security_response_id: String,
589    /// Security Request Result (Tag 560) - Always 0 for successful response
590    pub security_request_result: i32,
591    /// List of securities (Tag 146 - NoRelatedSym)
592    pub securities: Vec<SecurityInfo>,
593}
594
595impl SecurityList {
596    /// Create a new Security List response
597    pub fn new(
598        security_req_id: String,
599        security_response_id: String,
600        securities: Vec<SecurityInfo>,
601    ) -> Self {
602        Self {
603            security_req_id,
604            security_response_id,
605            security_request_result: 0, // Always 0 for successful response
606            securities,
607        }
608    }
609
610    /// Create a successful response
611    pub fn success(
612        security_req_id: String,
613        security_response_id: String,
614        securities: Vec<SecurityInfo>,
615    ) -> Self {
616        Self::new(security_req_id, security_response_id, securities)
617    }
618
619    /// Get number of securities in the list
620    pub fn count(&self) -> usize {
621        self.securities.len()
622    }
623
624    /// Check if the response is successful
625    pub fn is_successful(&self) -> bool {
626        self.security_request_result == 0
627    }
628
629    /// Filter securities by type
630    pub fn filter_by_type(&self, security_type: SecurityType) -> Vec<&SecurityInfo> {
631        self.securities
632            .iter()
633            .filter(|s| s.security_type == Some(security_type))
634            .collect()
635    }
636
637    /// Filter securities by currency
638    pub fn filter_by_currency(&self, currency: &str) -> Vec<&SecurityInfo> {
639        self.securities
640            .iter()
641            .filter(|s| s.currency.as_deref() == Some(currency))
642            .collect()
643    }
644
645    /// Convert to FIX message
646    pub fn to_fix_message(
647        &self,
648        sender_comp_id: String,
649        target_comp_id: String,
650        msg_seq_num: u32,
651    ) -> DeribitFixResult<crate::model::message::FixMessage> {
652        let mut builder = MessageBuilder::new()
653            .msg_type(MsgType::SecurityList)
654            .sender_comp_id(sender_comp_id)
655            .target_comp_id(target_comp_id)
656            .msg_seq_num(msg_seq_num)
657            .sending_time(Utc::now())
658            .field(320, self.security_req_id.clone()) // SecurityReqId
659            .field(322, self.security_response_id.clone()) // SecurityResponseID
660            .field(560, self.security_request_result.to_string()) // SecurityRequestResult
661            .field(146, self.securities.len().to_string()); // NoRelatedSym
662
663        // Add security information with proper FIX repeating group structure
664        for security in &self.securities {
665            // Required fields
666            builder = builder.field(55, security.symbol.clone()); // Symbol
667
668            // Optional security fields
669            if let Some(ref desc) = security.security_desc {
670                builder = builder.field(107, desc.clone()); // SecurityDesc
671            }
672
673            if let Some(ref sec_type) = security.security_type {
674                builder = builder.field(167, sec_type.as_fix_str().to_string()); // SecurityType
675            }
676
677            if let Some(put_or_call) = security.put_or_call {
678                builder = builder.field(201, i32::from(put_or_call).to_string()); // PutOrCall
679            }
680
681            if let Some(strike_price) = security.strike_price {
682                builder = builder.field(202, strike_price.to_string()); // StrikePrice
683            }
684
685            if let Some(ref strike_currency) = security.strike_currency {
686                builder = builder.field(947, strike_currency.clone()); // StrikeCurrency
687            }
688
689            if let Some(ref currency) = security.currency {
690                builder = builder.field(15, currency.clone()); // Currency
691            }
692
693            if let Some(ref price_quote_currency) = security.price_quote_currency {
694                builder = builder.field(1524, price_quote_currency.clone()); // PriceQuoteCurrency
695            }
696
697            if let Some(instrument_price_precision) = security.instrument_price_precision {
698                builder = builder.field(2576, instrument_price_precision.to_string()); // InstrumentPricePrecision
699            }
700
701            if let Some(min_price_increment) = security.min_price_increment {
702                builder = builder.field(969, min_price_increment.to_string()); // MinPriceIncrement
703            }
704
705            if let Some(ref underlying_symbol) = security.underlying_symbol {
706                builder = builder.field(311, underlying_symbol.clone()); // UnderlyingSymbol
707            }
708
709            if let Some(issue_date) = security.issue_date {
710                builder = builder.field(225, issue_date.format("%Y%m%d-%H:%M:%S%.3f").to_string()); // IssueDate
711            }
712
713            if let Some(maturity_date) = security.maturity_date {
714                builder = builder.field(541, maturity_date.format("%Y%m%d").to_string()); // MaturityDate
715            }
716
717            if let Some(maturity_time) = security.maturity_time {
718                builder = builder.field(
719                    1079,
720                    maturity_time.format("%Y%m%d-%H:%M:%S%.3f").to_string(),
721                ); // MaturityTime
722            }
723
724            if let Some(min_trade_vol) = security.min_trade_vol {
725                builder = builder.field(562, min_trade_vol.to_string()); // MinTradeVol
726            }
727
728            if let Some(ref settl_type) = security.settl_type {
729                builder = builder.field(63, settl_type.clone()); // SettlType
730            }
731
732            if let Some(ref settl_currency) = security.settl_currency {
733                builder = builder.field(120, settl_currency.clone()); // SettlCurrency
734            }
735
736            if let Some(ref comm_currency) = security.comm_currency {
737                builder = builder.field(479, comm_currency.clone()); // CommCurrency
738            }
739
740            if let Some(contract_multiplier) = security.contract_multiplier {
741                builder = builder.field(231, contract_multiplier.to_string()); // ContractMultiplier
742            }
743
744            // Security Alternative IDs repeating group
745            if !security.security_alt_ids.is_empty() {
746                builder = builder.field(454, security.security_alt_ids.len().to_string()); // NoSecurityAltID
747
748                for alt_id in &security.security_alt_ids {
749                    builder = builder.field(455, alt_id.security_alt_id.clone()); // SecurityAltID
750                    builder = builder.field(456, alt_id.security_alt_id_source.clone()); // SecurityAltIDSource
751                }
752            }
753
754            // Tick Rules repeating group
755            if !security.tick_rules.is_empty() {
756                builder = builder.field(1205, security.tick_rules.len().to_string()); // NoTickRules
757
758                for tick_rule in &security.tick_rules {
759                    builder = builder.field(1206, tick_rule.start_tick_price_range.to_string()); // StartTickPriceRange
760                    builder = builder.field(1208, tick_rule.tick_increment.to_string()); // TickIncrement
761                }
762            }
763
764            if let Some(security_status) = security.security_status {
765                builder = builder.field(965, i32::from(security_status).to_string()); // SecurityStatus
766            }
767        }
768
769        builder.build()
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_security_list_request_creation() {
779        let request = SecurityListRequest::snapshot("REQ123".to_string());
780        assert_eq!(request.security_req_id, "REQ123");
781        assert_eq!(
782            request.security_list_request_type,
783            SecurityListRequestType::Snapshot
784        );
785        assert!(request.subscription_request_type.is_none());
786    }
787
788    #[test]
789    fn test_security_list_request_subscription() {
790        let request = SecurityListRequest::subscription("SUB456".to_string());
791        assert_eq!(request.security_req_id, "SUB456");
792        assert_eq!(
793            request.security_list_request_type,
794            SecurityListRequestType::SnapshotAndUpdates
795        );
796        assert_eq!(
797            request.subscription_request_type,
798            Some(SubscriptionRequestType::SnapshotPlusUpdates)
799        );
800    }
801
802    #[test]
803    fn test_security_list_request_with_filters() {
804        let request = SecurityListRequest::snapshot("FILTER789".to_string())
805            .with_currency("BTC".to_string())
806            .with_security_type(SecurityType::Future)
807            .with_multicast_instrument_id(true);
808
809        assert_eq!(request.currency, Some("BTC".to_string()));
810        assert_eq!(request.security_type, Some(SecurityType::Future));
811        assert_eq!(request.display_multicast_instrument_id, Some(true));
812    }
813
814    #[test]
815    fn test_security_type_conversion() {
816        assert_eq!(SecurityType::Future.as_fix_str(), "FUT");
817        assert_eq!(SecurityType::Option.as_fix_str(), "OPT");
818        assert_eq!(SecurityType::FxSpot.as_fix_str(), "FXSPOT");
819
820        assert_eq!(
821            SecurityType::from_fix_str("FUT").unwrap(),
822            SecurityType::Future
823        );
824        assert_eq!(
825            SecurityType::from_fix_str("OPT").unwrap(),
826            SecurityType::Option
827        );
828        assert!(SecurityType::from_fix_str("INVALID").is_err());
829    }
830
831    #[test]
832    fn test_security_info_creation() {
833        let mut security = SecurityInfo::new("BTC-PERPETUAL".to_string());
834        security.security_type = Some(SecurityType::Future);
835        security.currency = Some("BTC".to_string());
836
837        assert_eq!(security.symbol, "BTC-PERPETUAL");
838        assert!(security.is_future());
839        assert!(!security.is_option());
840        assert!(!security.is_spot());
841    }
842
843    #[test]
844    fn test_security_list_response() {
845        let securities = vec![
846            SecurityInfo::new("BTC-PERPETUAL".to_string()),
847            SecurityInfo::new("ETH-PERPETUAL".to_string()),
848        ];
849
850        let response =
851            SecurityList::success("REQ123".to_string(), "RESP456".to_string(), securities);
852
853        assert_eq!(response.security_req_id, "REQ123");
854        assert_eq!(response.security_response_id, "RESP456");
855        assert_eq!(response.count(), 2);
856        assert!(response.is_successful());
857    }
858
859    #[test]
860    fn test_put_or_call_conversion() {
861        assert_eq!(i32::from(PutOrCall::Put), 0);
862        assert_eq!(i32::from(PutOrCall::Call), 1);
863        assert_eq!(PutOrCall::try_from(0).unwrap(), PutOrCall::Put);
864        assert_eq!(PutOrCall::try_from(1).unwrap(), PutOrCall::Call);
865        assert!(PutOrCall::try_from(2).is_err());
866    }
867
868    #[test]
869    fn test_security_status_conversion() {
870        assert_eq!(i32::from(SecurityStatus::Active), 1);
871        assert_eq!(i32::from(SecurityStatus::Terminated), 2);
872        assert_eq!(SecurityStatus::try_from(1).unwrap(), SecurityStatus::Active);
873        assert_eq!(
874            SecurityStatus::try_from(2).unwrap(),
875            SecurityStatus::Terminated
876        );
877        assert!(SecurityStatus::try_from(99).is_err());
878    }
879
880    #[test]
881    fn test_security_alt_id_creation() {
882        let multicast_id = SecurityAltId::multicast("MC123".to_string());
883        assert_eq!(multicast_id.security_alt_id, "MC123");
884        assert_eq!(multicast_id.security_alt_id_source, "101");
885
886        let combo_id = SecurityAltId::combo("COMBO456".to_string());
887        assert_eq!(combo_id.security_alt_id, "COMBO456");
888        assert_eq!(combo_id.security_alt_id_source, "102");
889    }
890
891    #[test]
892    fn test_tick_rule_creation() {
893        let tick_rule = TickRule {
894            start_tick_price_range: 100.0,
895            tick_increment: 0.5,
896        };
897
898        assert_eq!(tick_rule.start_tick_price_range, 100.0);
899        assert_eq!(tick_rule.tick_increment, 0.5);
900    }
901
902    #[test]
903    fn test_security_info_with_all_fields() {
904        use chrono::Utc;
905
906        let maturity_date = Utc::now() + chrono::Duration::days(30);
907        let issue_date = Utc::now() - chrono::Duration::days(365);
908
909        let security = SecurityInfo::new("BTC-28JUL23-30000-C".to_string())
910            .with_security_desc("BTC Call Option".to_string())
911            .with_security_type(SecurityType::Option)
912            .with_put_or_call(PutOrCall::Call)
913            .with_strike_price(30000.0)
914            .with_strike_currency("USD".to_string())
915            .with_currency("BTC".to_string())
916            .with_price_quote_currency("USD".to_string())
917            .with_instrument_price_precision(4)
918            .with_min_price_increment(0.0001)
919            .with_underlying_symbol("BTC".to_string())
920            .with_issue_date(issue_date)
921            .with_maturity_date(maturity_date)
922            .with_maturity_time(maturity_date)
923            .with_min_trade_vol(0.1)
924            .with_settl_type("M1".to_string())
925            .with_settl_currency("USD".to_string())
926            .with_comm_currency("USD".to_string())
927            .with_contract_multiplier(1.0)
928            .add_security_alt_id(SecurityAltId::multicast("MC123".to_string()))
929            .add_tick_rule(TickRule {
930                start_tick_price_range: 0.0,
931                tick_increment: 0.0001,
932            })
933            .with_security_status(SecurityStatus::Active);
934
935        assert_eq!(security.symbol, "BTC-28JUL23-30000-C");
936        assert_eq!(security.security_desc, Some("BTC Call Option".to_string()));
937        assert_eq!(security.security_type, Some(SecurityType::Option));
938        assert_eq!(security.put_or_call, Some(PutOrCall::Call));
939        assert_eq!(security.strike_price, Some(30000.0));
940        assert_eq!(security.strike_currency, Some("USD".to_string()));
941        assert_eq!(security.currency, Some("BTC".to_string()));
942        assert_eq!(security.price_quote_currency, Some("USD".to_string()));
943        assert_eq!(security.instrument_price_precision, Some(4));
944        assert_eq!(security.min_price_increment, Some(0.0001));
945        assert_eq!(security.underlying_symbol, Some("BTC".to_string()));
946        assert_eq!(security.issue_date, Some(issue_date));
947        assert_eq!(security.maturity_date, Some(maturity_date));
948        assert_eq!(security.maturity_time, Some(maturity_date));
949        assert_eq!(security.min_trade_vol, Some(0.1));
950        assert_eq!(security.settl_type, Some("M1".to_string()));
951        assert_eq!(security.settl_currency, Some("USD".to_string()));
952        assert_eq!(security.comm_currency, Some("USD".to_string()));
953        assert_eq!(security.contract_multiplier, Some(1.0));
954        assert_eq!(security.security_alt_ids.len(), 1);
955        assert_eq!(security.tick_rules.len(), 1);
956        assert_eq!(security.security_status, Some(SecurityStatus::Active));
957        assert!(security.is_option());
958    }
959
960    #[test]
961    fn test_security_list_to_fix_message_with_all_fields() {
962        let security = SecurityInfo::new("BTC-PERPETUAL".to_string())
963            .with_security_desc("BTC Perpetual Future".to_string())
964            .with_security_type(SecurityType::Future)
965            .with_currency("BTC".to_string())
966            .with_price_quote_currency("USD".to_string())
967            .with_instrument_price_precision(2)
968            .with_min_price_increment(0.5)
969            .with_min_trade_vol(1.0)
970            .with_settl_currency("USD".to_string())
971            .with_contract_multiplier(1.0)
972            .add_security_alt_id(SecurityAltId::multicast("MC456".to_string()))
973            .add_tick_rule(TickRule {
974                start_tick_price_range: 0.0,
975                tick_increment: 0.5,
976            })
977            .with_security_status(SecurityStatus::Active);
978
979        let securities = vec![security];
980        let security_list =
981            SecurityList::success("REQ123".to_string(), "RESP456".to_string(), securities);
982
983        let fix_message = security_list
984            .to_fix_message("SENDER".to_string(), "TARGET".to_string(), 1)
985            .unwrap();
986        let fix_str = fix_message.to_string();
987
988        // Check required SecurityList fields
989        assert!(fix_str.contains("35=y")); // MsgType
990        assert!(fix_str.contains("320=REQ123")); // SecurityReqId
991        assert!(fix_str.contains("322=RESP456")); // SecurityResponseID
992        assert!(fix_str.contains("560=0")); // SecurityRequestResult
993        assert!(fix_str.contains("146=1")); // NoRelatedSym
994
995        // Check SecurityInfo fields are serialized
996        assert!(fix_str.contains("55=BTC-PERPETUAL")); // Symbol
997        assert!(fix_str.contains("107=BTC Perpetual Future")); // SecurityDesc
998        assert!(fix_str.contains("167=FUT")); // SecurityType
999        assert!(fix_str.contains("15=BTC")); // Currency
1000        assert!(fix_str.contains("1524=USD")); // PriceQuoteCurrency
1001        assert!(fix_str.contains("2576=2")); // InstrumentPricePrecision
1002        assert!(fix_str.contains("969=0.5")); // MinPriceIncrement
1003        assert!(fix_str.contains("562=1")); // MinTradeVol
1004        assert!(fix_str.contains("120=USD")); // SettlCurrency
1005        assert!(fix_str.contains("231=1")); // ContractMultiplier
1006        assert!(fix_str.contains("454=1")); // NoSecurityAltID
1007        assert!(fix_str.contains("455=MC456")); // SecurityAltID
1008        assert!(fix_str.contains("456=101")); // SecurityAltIDSource (multicast)
1009        assert!(fix_str.contains("1205=1")); // NoTickRules
1010        assert!(fix_str.contains("1206=0")); // StartTickPriceRange
1011        assert!(fix_str.contains("1208=0.5")); // TickIncrement
1012        assert!(fix_str.contains("965=1")); // SecurityStatus
1013    }
1014}