deribit_fix/message/
market_data.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 22/7/25
5******************************************************************************/
6
7//! # Market Data FIX Messages
8//!
9//! This module implements the Market Data FIX protocol messages for Deribit according to the
10//! official FIX API specification. It includes:
11//!
12//! - Market Data Request (MsgType = 'V')
13//! - Market Data Request Reject (MsgType = 'Y')
14//! - Market Data Snapshot/Full Refresh (MsgType = 'W')
15//! - Market Data Incremental Refresh (MsgType = 'X')
16
17use crate::error::Result as DeribitFixResult;
18use crate::message::MessageBuilder;
19use crate::model::types::MsgType;
20use chrono::{DateTime, Utc};
21use serde::{Deserialize, Serialize};
22
23/// Market Data Subscription Request Type enumeration
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum MdSubscriptionRequestType {
26    /// Snapshot only
27    Snapshot = 0,
28    /// Snapshot + Updates (Subscribe)
29    SnapshotPlusUpdates = 1,
30    /// Disable previous Snapshot + Update Request (Unsubscribe)
31    Unsubscribe = 2,
32}
33
34impl From<MdSubscriptionRequestType> for i32 {
35    fn from(value: MdSubscriptionRequestType) -> Self {
36        value as i32
37    }
38}
39
40impl TryFrom<i32> for MdSubscriptionRequestType {
41    type Error = String;
42
43    fn try_from(value: i32) -> Result<Self, Self::Error> {
44        match value {
45            0 => Ok(MdSubscriptionRequestType::Snapshot),
46            1 => Ok(MdSubscriptionRequestType::SnapshotPlusUpdates),
47            2 => Ok(MdSubscriptionRequestType::Unsubscribe),
48            _ => Err(format!("Invalid MdSubscriptionRequestType: {value}")),
49        }
50    }
51}
52
53/// MD Update Type enumeration
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum MdUpdateType {
56    /// Full refresh
57    FullRefresh = 0,
58    /// Incremental refresh
59    IncrementalRefresh = 1,
60}
61
62impl From<MdUpdateType> for i32 {
63    fn from(value: MdUpdateType) -> Self {
64        value as i32
65    }
66}
67
68impl TryFrom<i32> for MdUpdateType {
69    type Error = String;
70
71    fn try_from(value: i32) -> Result<Self, Self::Error> {
72        match value {
73            0 => Ok(MdUpdateType::FullRefresh),
74            1 => Ok(MdUpdateType::IncrementalRefresh),
75            _ => Err(format!("Invalid MdUpdateType: {value}")),
76        }
77    }
78}
79
80/// MD Entry Type enumeration
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum MdEntryType {
83    /// Bid (Bid side of the order book)
84    Bid = 0,
85    /// Offer (Ask side of the order book)
86    Offer = 1,
87    /// Trade (Info about recent trades)
88    Trade = 2,
89    /// Index Value (value of Index for INDEX instruments)
90    IndexValue = 3,
91    /// Settlement Price (Estimated Delivery Price for INDEX instruments)
92    SettlementPrice = 6,
93}
94
95impl From<MdEntryType> for i32 {
96    fn from(value: MdEntryType) -> Self {
97        value as i32
98    }
99}
100
101impl TryFrom<i32> for MdEntryType {
102    type Error = String;
103
104    fn try_from(value: i32) -> Result<Self, Self::Error> {
105        match value {
106            0 => Ok(MdEntryType::Bid),
107            1 => Ok(MdEntryType::Offer),
108            2 => Ok(MdEntryType::Trade),
109            3 => Ok(MdEntryType::IndexValue),
110            6 => Ok(MdEntryType::SettlementPrice),
111            _ => Err(format!("Invalid MdEntryType: {value}")),
112        }
113    }
114}
115
116/// MD Update Action enumeration
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118pub enum MdUpdateAction {
119    /// New entry
120    New = 0,
121    /// Change existing entry
122    Change = 1,
123    /// Delete entry
124    Delete = 2,
125}
126
127impl From<MdUpdateAction> for char {
128    fn from(value: MdUpdateAction) -> Self {
129        match value {
130            MdUpdateAction::New => '0',
131            MdUpdateAction::Change => '1',
132            MdUpdateAction::Delete => '2',
133        }
134    }
135}
136
137impl TryFrom<char> for MdUpdateAction {
138    type Error = String;
139
140    fn try_from(value: char) -> Result<Self, Self::Error> {
141        match value {
142            '0' => Ok(MdUpdateAction::New),
143            '1' => Ok(MdUpdateAction::Change),
144            '2' => Ok(MdUpdateAction::Delete),
145            _ => Err(format!("Invalid MdUpdateAction: {value}")),
146        }
147    }
148}
149
150/// MD Request Reject Reason enumeration
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub enum MdReqRejReason {
153    /// Unknown symbol
154    UnknownSymbol = 0,
155    /// Duplicate MDReqID
156    DuplicateMdReqId = 1,
157    /// Insufficient Bandwidth
158    InsufficientBandwidth = 2,
159    /// Insufficient Permissions
160    InsufficientPermissions = 3,
161    /// Unsupported SubscriptionRequestType
162    UnsupportedSubscriptionRequestType = 4,
163    /// Unsupported MarketDepth
164    UnsupportedMarketDepth = 5,
165    /// Unsupported MDUpdateType
166    UnsupportedMdUpdateType = 6,
167    /// Unsupported AggregatedBook
168    UnsupportedAggregatedBook = 7,
169    /// Unsupported MDEntryType
170    UnsupportedMdEntryType = 8,
171    /// Unsupported TradingSessionID
172    UnsupportedTradingSessionId = 9,
173    /// Unsupported Scope
174    UnsupportedScope = 10,
175    /// Unsupported OpenCloseSettlFlag
176    UnsupportedOpenCloseSettlFlag = 11,
177    /// Unsupported MDImplicitDelete
178    UnsupportedMdImplicitDelete = 12,
179    /// Insufficient credit
180    InsufficientCredit = 13,
181}
182
183impl From<MdReqRejReason> for char {
184    fn from(value: MdReqRejReason) -> Self {
185        match value {
186            MdReqRejReason::UnknownSymbol => '0',
187            MdReqRejReason::DuplicateMdReqId => '1',
188            MdReqRejReason::InsufficientBandwidth => '2',
189            MdReqRejReason::InsufficientPermissions => '3',
190            MdReqRejReason::UnsupportedSubscriptionRequestType => '4',
191            MdReqRejReason::UnsupportedMarketDepth => '5',
192            MdReqRejReason::UnsupportedMdUpdateType => '6',
193            MdReqRejReason::UnsupportedAggregatedBook => '7',
194            MdReqRejReason::UnsupportedMdEntryType => '8',
195            MdReqRejReason::UnsupportedTradingSessionId => '9',
196            MdReqRejReason::UnsupportedScope => 'A',
197            MdReqRejReason::UnsupportedOpenCloseSettlFlag => 'B',
198            MdReqRejReason::UnsupportedMdImplicitDelete => 'C',
199            MdReqRejReason::InsufficientCredit => 'D',
200        }
201    }
202}
203
204impl TryFrom<char> for MdReqRejReason {
205    type Error = String;
206
207    fn try_from(value: char) -> Result<Self, Self::Error> {
208        match value {
209            '0' => Ok(MdReqRejReason::UnknownSymbol),
210            '1' => Ok(MdReqRejReason::DuplicateMdReqId),
211            '2' => Ok(MdReqRejReason::InsufficientBandwidth),
212            '3' => Ok(MdReqRejReason::InsufficientPermissions),
213            '4' => Ok(MdReqRejReason::UnsupportedSubscriptionRequestType),
214            '5' => Ok(MdReqRejReason::UnsupportedMarketDepth),
215            '6' => Ok(MdReqRejReason::UnsupportedMdUpdateType),
216            '7' => Ok(MdReqRejReason::UnsupportedAggregatedBook),
217            '8' => Ok(MdReqRejReason::UnsupportedMdEntryType),
218            '9' => Ok(MdReqRejReason::UnsupportedTradingSessionId),
219            'A' => Ok(MdReqRejReason::UnsupportedScope),
220            'B' => Ok(MdReqRejReason::UnsupportedOpenCloseSettlFlag),
221            'C' => Ok(MdReqRejReason::UnsupportedMdImplicitDelete),
222            'D' => Ok(MdReqRejReason::InsufficientCredit),
223            _ => Err(format!("Invalid MdReqRejReason: {value}")),
224        }
225    }
226}
227
228/// Market Data Request message structure
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct MarketDataRequest {
231    /// Unique ID assigned to this request
232    pub md_req_id: String,
233    /// Subscription Request Type
234    pub subscription_request_type: MdSubscriptionRequestType,
235    /// Market depth (optional)
236    pub market_depth: Option<i32>,
237    /// MD Update Type (when SubscriptionRequestType=1)
238    pub md_update_type: Option<MdUpdateType>,
239    /// Skip block trades flag
240    pub skip_block_trades: Option<bool>,
241    /// Show block trade ID flag
242    pub show_block_trade_id: Option<bool>,
243    /// Amount of trades returned in snapshot (default 20, max 1000)
244    pub trade_amount: Option<i32>,
245    /// UTC timestamp in milliseconds for trades since timestamp
246    pub since_timestamp: Option<i64>,
247    /// Entry types requested
248    pub entry_types: Vec<MdEntryType>,
249    /// Symbols requested
250    pub symbols: Vec<String>,
251}
252
253impl MarketDataRequest {
254    /// Create a new snapshot request
255    pub fn snapshot(
256        md_req_id: String,
257        symbols: Vec<String>,
258        entry_types: Vec<MdEntryType>,
259    ) -> Self {
260        Self {
261            md_req_id,
262            subscription_request_type: MdSubscriptionRequestType::Snapshot,
263            market_depth: None,
264            md_update_type: None,
265            skip_block_trades: None,
266            show_block_trade_id: None,
267            trade_amount: None,
268            since_timestamp: None,
269            entry_types,
270            symbols,
271        }
272    }
273
274    /// Create a new subscription request
275    pub fn subscription(
276        md_req_id: String,
277        symbols: Vec<String>,
278        entry_types: Vec<MdEntryType>,
279        md_update_type: MdUpdateType,
280    ) -> Self {
281        Self {
282            md_req_id,
283            subscription_request_type: MdSubscriptionRequestType::SnapshotPlusUpdates,
284            market_depth: None,
285            md_update_type: Some(md_update_type),
286            skip_block_trades: None,
287            show_block_trade_id: None,
288            trade_amount: None,
289            since_timestamp: None,
290            entry_types,
291            symbols,
292        }
293    }
294
295    /// Create an unsubscribe request
296    pub fn unsubscribe(md_req_id: String) -> Self {
297        Self {
298            md_req_id,
299            subscription_request_type: MdSubscriptionRequestType::Unsubscribe,
300            market_depth: None,
301            md_update_type: None,
302            skip_block_trades: None,
303            show_block_trade_id: None,
304            trade_amount: None,
305            since_timestamp: None,
306            entry_types: Vec::new(),
307            symbols: Vec::new(),
308        }
309    }
310
311    /// Convert to FIX message
312    pub fn to_fix_message(
313        &self,
314        sender_comp_id: String,
315        target_comp_id: String,
316        msg_seq_num: u32,
317    ) -> DeribitFixResult<String> {
318        let mut builder = MessageBuilder::new()
319            .msg_type(MsgType::MarketDataRequest)
320            .sender_comp_id(sender_comp_id)
321            .target_comp_id(target_comp_id)
322            .msg_seq_num(msg_seq_num)
323            .sending_time(Utc::now())
324            .field(262, self.md_req_id.clone()) // MDReqID
325            .field(263, i32::from(self.subscription_request_type).to_string()); // SubscriptionRequestType
326
327        // Add optional fields
328        if let Some(depth) = self.market_depth {
329            builder = builder.field(264, depth.to_string()); // MarketDepth
330        }
331
332        if let Some(update_type) = self.md_update_type {
333            builder = builder.field(265, i32::from(update_type).to_string()); // MDUpdateType
334        }
335
336        // Add entry types group
337        builder = builder.field(267, self.entry_types.len().to_string()); // NoMDEntryTypes
338        for entry_type in &self.entry_types {
339            builder = builder.field(269, i32::from(*entry_type).to_string()); // MDEntryType
340        }
341
342        // Add Deribit-specific optional fields
343        if let Some(skip_block_trades) = self.skip_block_trades {
344            builder = builder.field(9011, if skip_block_trades { "Y" } else { "N" }.to_string()); // DeribitSkipBlockTrades
345        }
346
347        if let Some(show_block_trade_id) = self.show_block_trade_id {
348            builder = builder.field(
349                9012,
350                if show_block_trade_id { "Y" } else { "N" }.to_string(),
351            ); // DeribitShowBlockTradeId
352        }
353
354        if let Some(trade_amount) = self.trade_amount {
355            builder = builder.field(100007, trade_amount.to_string()); // DeribitTradeAmount
356        }
357
358        if let Some(since_timestamp) = self.since_timestamp {
359            builder = builder.field(100008, since_timestamp.to_string()); // DeribitSinceTimestamp
360        }
361
362        // Add symbols group
363        if !self.symbols.is_empty() {
364            builder = builder.field(146, self.symbols.len().to_string()); // NoRelatedSym
365            for symbol in &self.symbols {
366                builder = builder.field(55, symbol.clone()); // Symbol
367            }
368        }
369
370        Ok(builder.build()?.to_string())
371    }
372}
373
374/// Market Data Request Reject message structure
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct MarketDataRequestReject {
377    /// ID of the original request
378    pub md_req_id: String,
379    /// Reason for rejection
380    pub md_req_rej_reason: MdReqRejReason,
381    /// Free format text string
382    pub text: Option<String>,
383}
384
385impl MarketDataRequestReject {
386    /// Create a new reject message
387    pub fn new(md_req_id: String, reason: MdReqRejReason) -> Self {
388        Self {
389            md_req_id,
390            md_req_rej_reason: reason,
391            text: None,
392        }
393    }
394
395    /// Create a reject message with text
396    pub fn with_text(md_req_id: String, reason: MdReqRejReason, text: String) -> Self {
397        Self {
398            md_req_id,
399            md_req_rej_reason: reason,
400            text: Some(text),
401        }
402    }
403
404    /// Convert to FIX message
405    pub fn to_fix_message(
406        &self,
407        sender_comp_id: String,
408        target_comp_id: String,
409        msg_seq_num: u32,
410    ) -> DeribitFixResult<String> {
411        let mut builder = MessageBuilder::new()
412            .msg_type(MsgType::MarketDataRequestReject)
413            .sender_comp_id(sender_comp_id)
414            .target_comp_id(target_comp_id)
415            .msg_seq_num(msg_seq_num)
416            .sending_time(Utc::now())
417            .field(262, self.md_req_id.clone()) // MDReqID
418            .field(281, char::from(self.md_req_rej_reason).to_string()); // MDReqRejReason
419
420        if let Some(ref text) = self.text {
421            builder = builder.field(58, text.clone()); // Text
422        }
423
424        Ok(builder.build()?.to_string())
425    }
426}
427
428/// Market Data Entry for snapshot and incremental messages
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct MdEntry {
431    /// Entry type
432    pub md_entry_type: MdEntryType,
433    /// Price of entry (optional)
434    pub md_entry_px: Option<f64>,
435    /// Size of entry (optional)
436    pub md_entry_size: Option<f64>,
437    /// Timestamp for entry (optional)
438    pub md_entry_date: Option<DateTime<Utc>>,
439    /// Update action (for incremental refresh)
440    pub md_update_action: Option<MdUpdateAction>,
441    /// Trade ID (for trades)
442    pub trade_id: Option<String>,
443    /// Side (for trades)
444    pub side: Option<char>,
445    /// Order ID (for trades)
446    pub order_id: Option<String>,
447    /// Secondary order ID (for trades)
448    pub secondary_order_id: Option<String>,
449    /// Index price at trade moment (snapshot-only)
450    pub price: Option<f64>,
451    /// Trade sequence number (snapshot-only)
452    pub text: Option<String>,
453    /// Order status (snapshot-only)
454    pub ord_status: Option<char>,
455    /// User-defined order label (snapshot-only)
456    pub deribit_label: Option<String>,
457    /// Liquidation indicator (snapshot-only)
458    pub deribit_liquidation: Option<String>,
459    /// Block trade ID (snapshot-only)
460    pub trd_match_id: Option<String>,
461}
462
463impl MdEntry {
464    /// Create a new bid entry
465    pub fn bid(price: f64, size: f64) -> Self {
466        Self {
467            md_entry_type: MdEntryType::Bid,
468            md_entry_px: Some(price),
469            md_entry_size: Some(size),
470            md_entry_date: None,
471            md_update_action: None,
472            trade_id: None,
473            side: None,
474            order_id: None,
475            secondary_order_id: None,
476            price: None,
477            text: None,
478            ord_status: None,
479            deribit_label: None,
480            deribit_liquidation: None,
481            trd_match_id: None,
482        }
483    }
484
485    /// Create a new offer entry
486    pub fn offer(price: f64, size: f64) -> Self {
487        Self {
488            md_entry_type: MdEntryType::Offer,
489            md_entry_px: Some(price),
490            md_entry_size: Some(size),
491            md_entry_date: None,
492            md_update_action: None,
493            trade_id: None,
494            side: None,
495            order_id: None,
496            secondary_order_id: None,
497            price: None,
498            text: None,
499            ord_status: None,
500            deribit_label: None,
501            deribit_liquidation: None,
502            trd_match_id: None,
503        }
504    }
505
506    /// Create a new trade entry
507    pub fn trade(
508        price: f64,
509        size: f64,
510        side: char,
511        trade_id: String,
512        timestamp: DateTime<Utc>,
513    ) -> Self {
514        Self {
515            md_entry_type: MdEntryType::Trade,
516            md_entry_px: Some(price),
517            md_entry_size: Some(size),
518            md_entry_date: Some(timestamp),
519            md_update_action: None,
520            trade_id: Some(trade_id),
521            side: Some(side),
522            order_id: None,
523            secondary_order_id: None,
524            price: None,
525            text: None,
526            ord_status: None,
527            deribit_label: None,
528            deribit_liquidation: None,
529            trd_match_id: None,
530        }
531    }
532
533    /// Set update action for incremental refresh
534    pub fn with_update_action(mut self, action: MdUpdateAction) -> Self {
535        self.md_update_action = Some(action);
536        self
537    }
538}
539
540/// Market Data Snapshot/Full Refresh message structure
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct MarketDataSnapshotFullRefresh {
543    /// Instrument symbol
544    pub symbol: String,
545    /// ID of the original request (optional)
546    pub md_req_id: Option<String>,
547    /// Underlying symbol (for options)
548    pub underlying_symbol: Option<String>,
549    /// Price of the underlying instrument (for options)
550    pub underlying_px: Option<f64>,
551    /// Contract multiplier
552    pub contract_multiplier: Option<f64>,
553    /// Put or Call indicator (0 = put, 1 = call)
554    pub put_or_call: Option<i32>,
555    /// 24h trade volume
556    pub trade_volume_24h: Option<f64>,
557    /// Mark price
558    pub mark_price: Option<f64>,
559    /// Open interest
560    pub open_interest: Option<f64>,
561    /// Current funding (perpetual only)
562    pub current_funding: Option<f64>,
563    /// Funding in last 8h (perpetual only)
564    pub funding_8h: Option<f64>,
565    /// Market data entries
566    pub entries: Vec<MdEntry>,
567}
568
569impl MarketDataSnapshotFullRefresh {
570    /// Create a new snapshot message
571    pub fn new(symbol: String) -> Self {
572        Self {
573            symbol,
574            md_req_id: None,
575            underlying_symbol: None,
576            underlying_px: None,
577            contract_multiplier: None,
578            put_or_call: None,
579            trade_volume_24h: None,
580            mark_price: None,
581            open_interest: None,
582            current_funding: None,
583            funding_8h: None,
584            entries: Vec::new(),
585        }
586    }
587
588    /// Set request ID
589    pub fn with_request_id(mut self, md_req_id: String) -> Self {
590        self.md_req_id = Some(md_req_id);
591        self
592    }
593
594    /// Add entries
595    pub fn with_entries(mut self, entries: Vec<MdEntry>) -> Self {
596        self.entries = entries;
597        self
598    }
599
600    /// Set underlying symbol (for options)
601    pub fn with_underlying_symbol(mut self, underlying_symbol: String) -> Self {
602        self.underlying_symbol = Some(underlying_symbol);
603        self
604    }
605
606    /// Set underlying price (for options)
607    pub fn with_underlying_px(mut self, underlying_px: f64) -> Self {
608        self.underlying_px = Some(underlying_px);
609        self
610    }
611
612    /// Set contract multiplier
613    pub fn with_contract_multiplier(mut self, contract_multiplier: f64) -> Self {
614        self.contract_multiplier = Some(contract_multiplier);
615        self
616    }
617
618    /// Set put or call indicator (0 = put, 1 = call)
619    pub fn with_put_or_call(mut self, put_or_call: i32) -> Self {
620        self.put_or_call = Some(put_or_call);
621        self
622    }
623
624    /// Set 24h trade volume
625    pub fn with_trade_volume_24h(mut self, trade_volume_24h: f64) -> Self {
626        self.trade_volume_24h = Some(trade_volume_24h);
627        self
628    }
629
630    /// Set mark price
631    pub fn with_mark_price(mut self, mark_price: f64) -> Self {
632        self.mark_price = Some(mark_price);
633        self
634    }
635
636    /// Set open interest
637    pub fn with_open_interest(mut self, open_interest: f64) -> Self {
638        self.open_interest = Some(open_interest);
639        self
640    }
641
642    /// Set current funding (perpetual only)
643    pub fn with_current_funding(mut self, current_funding: f64) -> Self {
644        self.current_funding = Some(current_funding);
645        self
646    }
647
648    /// Set funding in last 8h (perpetual only)
649    pub fn with_funding_8h(mut self, funding_8h: f64) -> Self {
650        self.funding_8h = Some(funding_8h);
651        self
652    }
653
654    /// Convert to FIX message
655    pub fn to_fix_message(
656        &self,
657        sender_comp_id: String,
658        target_comp_id: String,
659        msg_seq_num: u32,
660    ) -> DeribitFixResult<String> {
661        let mut builder = MessageBuilder::new()
662            .msg_type(MsgType::MarketDataSnapshotFullRefresh)
663            .sender_comp_id(sender_comp_id)
664            .target_comp_id(target_comp_id)
665            .msg_seq_num(msg_seq_num)
666            .sending_time(Utc::now())
667            .field(55, self.symbol.clone()); // Symbol
668
669        if let Some(ref md_req_id) = self.md_req_id {
670            builder = builder.field(262, md_req_id.clone()); // MDReqID
671        }
672
673        // Add optional snapshot-specific fields
674        if let Some(ref underlying_symbol) = self.underlying_symbol {
675            builder = builder.field(311, underlying_symbol.clone()); // UnderlyingSymbol
676        }
677
678        if let Some(underlying_px) = self.underlying_px {
679            builder = builder.field(810, underlying_px.to_string()); // UnderlyingPx
680        }
681
682        if let Some(contract_multiplier) = self.contract_multiplier {
683            builder = builder.field(231, contract_multiplier.to_string()); // ContractMultiplier
684        }
685
686        if let Some(put_or_call) = self.put_or_call {
687            builder = builder.field(201, put_or_call.to_string()); // PutOrCall
688        }
689
690        if let Some(trade_volume_24h) = self.trade_volume_24h {
691            builder = builder.field(100087, trade_volume_24h.to_string()); // TradeVolume24h
692        }
693
694        if let Some(mark_price) = self.mark_price {
695            builder = builder.field(100090, mark_price.to_string()); // MarkPrice
696        }
697
698        if let Some(open_interest) = self.open_interest {
699            builder = builder.field(746, open_interest.to_string()); // OpenInterest
700        }
701
702        if let Some(current_funding) = self.current_funding {
703            builder = builder.field(100092, current_funding.to_string()); // CurrentFunding
704        }
705
706        if let Some(funding_8h) = self.funding_8h {
707            builder = builder.field(100093, funding_8h.to_string()); // Funding8h
708        }
709
710        // Add entries group
711        builder = builder.field(268, self.entries.len().to_string()); // NoMDEntries
712
713        for entry in &self.entries {
714            builder = builder.field(269, i32::from(entry.md_entry_type).to_string()); // MDEntryType
715
716            if let Some(px) = entry.md_entry_px {
717                builder = builder.field(270, px.to_string()); // MDEntryPx
718            }
719
720            if let Some(size) = entry.md_entry_size {
721                builder = builder.field(271, size.to_string()); // MDEntrySize
722            }
723
724            if let Some(date) = entry.md_entry_date {
725                builder = builder.field(272, date.timestamp_millis().to_string()); // MDEntryDate
726            }
727
728            if let Some(ref trade_id) = entry.trade_id {
729                builder = builder.field(100009, trade_id.clone()); // DeribitTradeId
730            }
731
732            if let Some(side) = entry.side {
733                builder = builder.field(54, side.to_string()); // Side
734            }
735
736            // Snapshot-only optional fields
737            if let Some(price) = entry.price {
738                builder = builder.field(44, price.to_string()); // Price (index price at trade moment)
739            }
740
741            if let Some(ref text) = entry.text {
742                builder = builder.field(58, text.clone()); // Text (trade sequence number)
743            }
744
745            if let Some(ref order_id) = entry.order_id {
746                builder = builder.field(37, order_id.clone()); // OrderId (taker's matching order id)
747            }
748
749            if let Some(ref secondary_order_id) = entry.secondary_order_id {
750                builder = builder.field(198, secondary_order_id.clone()); // SecondaryOrderId (maker's matching order id)
751            }
752
753            if let Some(ord_status) = entry.ord_status {
754                builder = builder.field(39, ord_status.to_string()); // OrdStatus (order status)
755            }
756
757            if let Some(ref deribit_label) = entry.deribit_label {
758                builder = builder.field(100010, deribit_label.clone()); // DeribitLabel (user defined label)
759            }
760
761            if let Some(ref deribit_liquidation) = entry.deribit_liquidation {
762                builder = builder.field(100091, deribit_liquidation.clone()); // DeribitLiquidation (liquidation indicator)
763            }
764
765            if let Some(ref trd_match_id) = entry.trd_match_id {
766                builder = builder.field(880, trd_match_id.clone()); // TrdMatchID (block trade id)
767            }
768        }
769
770        Ok(builder.build()?.to_string())
771    }
772}
773
774/// Market Data Incremental Refresh message structure
775#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct MarketDataIncrementalRefresh {
777    /// Instrument symbol
778    pub symbol: String,
779    /// ID of the original request (optional)
780    pub md_req_id: Option<String>,
781    /// Market data entries with update actions
782    pub entries: Vec<MdEntry>,
783}
784
785impl MarketDataIncrementalRefresh {
786    /// Create a new incremental refresh message
787    pub fn new(symbol: String) -> Self {
788        Self {
789            symbol,
790            md_req_id: None,
791            entries: Vec::new(),
792        }
793    }
794
795    /// Set request ID
796    pub fn with_request_id(mut self, md_req_id: String) -> Self {
797        self.md_req_id = Some(md_req_id);
798        self
799    }
800
801    /// Add entries
802    pub fn with_entries(mut self, entries: Vec<MdEntry>) -> Self {
803        self.entries = entries;
804        self
805    }
806
807    /// Convert to FIX message
808    pub fn to_fix_message(
809        &self,
810        sender_comp_id: String,
811        target_comp_id: String,
812        msg_seq_num: u32,
813    ) -> DeribitFixResult<String> {
814        let mut builder = MessageBuilder::new()
815            .msg_type(MsgType::MarketDataIncrementalRefresh)
816            .sender_comp_id(sender_comp_id)
817            .target_comp_id(target_comp_id)
818            .msg_seq_num(msg_seq_num)
819            .sending_time(Utc::now())
820            .field(55, self.symbol.clone()); // Symbol
821
822        if let Some(ref md_req_id) = self.md_req_id {
823            builder = builder.field(262, md_req_id.clone()); // MDReqID
824        }
825
826        // Add entries group
827        builder = builder.field(268, self.entries.len().to_string()); // NoMDEntries
828
829        for entry in &self.entries {
830            if let Some(action) = entry.md_update_action {
831                builder = builder.field(279, char::from(action).to_string()); // MDUpdateAction
832            }
833
834            builder = builder.field(269, i32::from(entry.md_entry_type).to_string()); // MDEntryType
835
836            if let Some(px) = entry.md_entry_px {
837                builder = builder.field(270, px.to_string()); // MDEntryPx
838            }
839
840            if let Some(size) = entry.md_entry_size {
841                builder = builder.field(271, size.to_string()); // MDEntrySize
842            }
843
844            if let Some(date) = entry.md_entry_date {
845                builder = builder.field(272, date.timestamp_millis().to_string()); // MDEntryDate
846            }
847
848            if let Some(ref trade_id) = entry.trade_id {
849                builder = builder.field(100009, trade_id.clone()); // DeribitTradeId
850            }
851
852            if let Some(side) = entry.side {
853                builder = builder.field(54, side.to_string()); // Side
854            }
855        }
856
857        Ok(builder.build()?.to_string())
858    }
859}
860
861#[cfg(test)]
862mod tests {
863    use super::*;
864
865    #[test]
866    fn test_md_subscription_request_type_conversion() {
867        assert_eq!(i32::from(MdSubscriptionRequestType::Snapshot), 0);
868        assert_eq!(i32::from(MdSubscriptionRequestType::SnapshotPlusUpdates), 1);
869        assert_eq!(i32::from(MdSubscriptionRequestType::Unsubscribe), 2);
870
871        assert_eq!(
872            MdSubscriptionRequestType::try_from(0).unwrap(),
873            MdSubscriptionRequestType::Snapshot
874        );
875        assert_eq!(
876            MdSubscriptionRequestType::try_from(1).unwrap(),
877            MdSubscriptionRequestType::SnapshotPlusUpdates
878        );
879        assert_eq!(
880            MdSubscriptionRequestType::try_from(2).unwrap(),
881            MdSubscriptionRequestType::Unsubscribe
882        );
883        assert!(MdSubscriptionRequestType::try_from(99).is_err());
884    }
885
886    #[test]
887    fn test_md_entry_type_conversion() {
888        assert_eq!(i32::from(MdEntryType::Bid), 0);
889        assert_eq!(i32::from(MdEntryType::Offer), 1);
890        assert_eq!(i32::from(MdEntryType::Trade), 2);
891
892        assert_eq!(MdEntryType::try_from(0).unwrap(), MdEntryType::Bid);
893        assert_eq!(MdEntryType::try_from(1).unwrap(), MdEntryType::Offer);
894        assert_eq!(MdEntryType::try_from(2).unwrap(), MdEntryType::Trade);
895        assert!(MdEntryType::try_from(99).is_err());
896    }
897
898    #[test]
899    fn test_market_data_request_creation() {
900        let request = MarketDataRequest::snapshot(
901            "REQ123".to_string(),
902            vec!["BTC-PERPETUAL".to_string()],
903            vec![MdEntryType::Bid, MdEntryType::Offer],
904        );
905        assert_eq!(request.md_req_id, "REQ123");
906        assert_eq!(
907            request.subscription_request_type,
908            MdSubscriptionRequestType::Snapshot
909        );
910        assert_eq!(request.symbols.len(), 1);
911        assert_eq!(request.entry_types.len(), 2);
912    }
913
914    #[test]
915    fn test_market_data_request_reject_creation() {
916        let reject =
917            MarketDataRequestReject::new("REQ123".to_string(), MdReqRejReason::UnknownSymbol);
918        assert_eq!(reject.md_req_id, "REQ123");
919        assert_eq!(reject.md_req_rej_reason, MdReqRejReason::UnknownSymbol);
920        assert!(reject.text.is_none());
921    }
922
923    #[test]
924    fn test_md_req_rej_reason_conversion() {
925        assert_eq!(char::from(MdReqRejReason::UnknownSymbol), '0');
926        assert_eq!(char::from(MdReqRejReason::InsufficientCredit), 'D');
927
928        assert_eq!(
929            MdReqRejReason::try_from('0').unwrap(),
930            MdReqRejReason::UnknownSymbol
931        );
932        assert_eq!(
933            MdReqRejReason::try_from('D').unwrap(),
934            MdReqRejReason::InsufficientCredit
935        );
936        assert!(MdReqRejReason::try_from('Z').is_err());
937    }
938
939    #[test]
940    fn test_md_entry_creation() {
941        let bid = MdEntry::bid(50000.0, 1.5);
942        assert_eq!(bid.md_entry_type, MdEntryType::Bid);
943        assert_eq!(bid.md_entry_px, Some(50000.0));
944        assert_eq!(bid.md_entry_size, Some(1.5));
945        assert!(bid.md_update_action.is_none());
946
947        let offer = MdEntry::offer(50100.0, 2.0);
948        assert_eq!(offer.md_entry_type, MdEntryType::Offer);
949        assert_eq!(offer.md_entry_px, Some(50100.0));
950        assert_eq!(offer.md_entry_size, Some(2.0));
951    }
952
953    #[test]
954    fn test_market_data_snapshot_creation() {
955        let snapshot = MarketDataSnapshotFullRefresh::new("BTC-PERPETUAL".to_string())
956            .with_request_id("REQ123".to_string())
957            .with_entries(vec![
958                MdEntry::bid(50000.0, 1.0),
959                MdEntry::offer(50100.0, 1.5),
960            ]);
961
962        assert_eq!(snapshot.symbol, "BTC-PERPETUAL");
963        assert_eq!(snapshot.md_req_id, Some("REQ123".to_string()));
964        assert_eq!(snapshot.entries.len(), 2);
965    }
966
967    #[test]
968    fn test_market_data_incremental_creation() {
969        let incremental = MarketDataIncrementalRefresh::new("BTC-PERPETUAL".to_string())
970            .with_entries(vec![
971                MdEntry::bid(50000.0, 1.0).with_update_action(MdUpdateAction::New),
972                MdEntry::offer(50100.0, 1.5).with_update_action(MdUpdateAction::Change),
973            ]);
974
975        assert_eq!(incremental.symbol, "BTC-PERPETUAL");
976        assert_eq!(incremental.entries.len(), 2);
977        assert_eq!(
978            incremental.entries[0].md_update_action,
979            Some(MdUpdateAction::New)
980        );
981        assert_eq!(
982            incremental.entries[1].md_update_action,
983            Some(MdUpdateAction::Change)
984        );
985    }
986
987    #[test]
988    fn test_md_update_action_conversion() {
989        assert_eq!(char::from(MdUpdateAction::New), '0');
990        assert_eq!(char::from(MdUpdateAction::Change), '1');
991        assert_eq!(char::from(MdUpdateAction::Delete), '2');
992
993        assert_eq!(MdUpdateAction::try_from('0').unwrap(), MdUpdateAction::New);
994        assert_eq!(
995            MdUpdateAction::try_from('1').unwrap(),
996            MdUpdateAction::Change
997        );
998        assert_eq!(
999            MdUpdateAction::try_from('2').unwrap(),
1000            MdUpdateAction::Delete
1001        );
1002        assert!(MdUpdateAction::try_from('9').is_err());
1003    }
1004}