deribit_fix/message/quotes/
quote_request.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 10/8/25
5******************************************************************************/
6
7//! Quote Request FIX Message Implementation
8
9use crate::error::Result as DeribitFixResult;
10use crate::message::builder::MessageBuilder;
11use crate::message::orders::{OrderSide, TimeInForce};
12use crate::model::types::MsgType;
13use chrono::{DateTime, Utc};
14use deribit_base::{impl_json_debug_pretty, impl_json_display};
15use serde::{Deserialize, Serialize};
16
17/// Quote type enumeration
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum QuoteType {
20    /// Indicative quote
21    Indicative,
22    /// Tradeable quote
23    Tradeable,
24    /// Restricted tradeable quote
25    RestrictedTradeable,
26    /// Counter quote
27    Counter,
28}
29
30impl From<QuoteType> for i32 {
31    fn from(quote_type: QuoteType) -> Self {
32        match quote_type {
33            QuoteType::Indicative => 0,
34            QuoteType::Tradeable => 1,
35            QuoteType::RestrictedTradeable => 2,
36            QuoteType::Counter => 3,
37        }
38    }
39}
40
41impl TryFrom<i32> for QuoteType {
42    type Error = String;
43
44    fn try_from(value: i32) -> Result<Self, Self::Error> {
45        match value {
46            0 => Ok(QuoteType::Indicative),
47            1 => Ok(QuoteType::Tradeable),
48            2 => Ok(QuoteType::RestrictedTradeable),
49            3 => Ok(QuoteType::Counter),
50            _ => Err(format!("Invalid QuoteType: {}", value)),
51        }
52    }
53}
54
55/// Quote Request message (MsgType = 'R')
56#[derive(Clone, PartialEq, Serialize, Deserialize)]
57pub struct QuoteRequest {
58    /// Quote request ID
59    pub quote_req_id: String,
60    /// Instrument symbol
61    pub symbol: String,
62    /// Quote type
63    pub quote_type: QuoteType,
64    /// Side of quote request
65    pub side: OrderSide,
66    /// Order quantity
67    pub order_qty: f64,
68    /// Valid until time
69    pub valid_until_time: Option<DateTime<Utc>>,
70    /// Quote request type (0=Manual, 1=Automatic)
71    pub quote_request_type: Option<i32>,
72    /// Time in force
73    pub time_in_force: Option<TimeInForce>,
74    /// Minimum quantity
75    pub min_qty: Option<f64>,
76    /// Settlement type
77    pub settlement_type: Option<char>,
78    /// Custom label
79    pub deribit_label: Option<String>,
80    /// Market segment ID
81    pub market_segment_id: Option<String>,
82}
83
84impl QuoteRequest {
85    /// Create a new quote request
86    pub fn new(
87        quote_req_id: String,
88        symbol: String,
89        quote_type: QuoteType,
90        side: OrderSide,
91        order_qty: f64,
92    ) -> Self {
93        Self {
94            quote_req_id,
95            symbol,
96            quote_type,
97            side,
98            order_qty,
99            valid_until_time: None,
100            quote_request_type: None,
101            time_in_force: None,
102            min_qty: None,
103            settlement_type: None,
104            deribit_label: None,
105            market_segment_id: None,
106        }
107    }
108
109    /// Create a tradeable quote request
110    pub fn tradeable(
111        quote_req_id: String,
112        symbol: String,
113        side: OrderSide,
114        order_qty: f64,
115    ) -> Self {
116        Self::new(quote_req_id, symbol, QuoteType::Tradeable, side, order_qty)
117    }
118
119    /// Create an indicative quote request
120    pub fn indicative(
121        quote_req_id: String,
122        symbol: String,
123        side: OrderSide,
124        order_qty: f64,
125    ) -> Self {
126        Self::new(quote_req_id, symbol, QuoteType::Indicative, side, order_qty)
127    }
128
129    /// Set valid until time
130    pub fn with_valid_until(mut self, valid_until: DateTime<Utc>) -> Self {
131        self.valid_until_time = Some(valid_until);
132        self
133    }
134
135    /// Set quote request type
136    pub fn with_quote_request_type(mut self, request_type: i32) -> Self {
137        self.quote_request_type = Some(request_type);
138        self
139    }
140
141    /// Set time in force
142    pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
143        self.time_in_force = Some(tif);
144        self
145    }
146
147    /// Set minimum quantity
148    pub fn with_min_qty(mut self, min_qty: f64) -> Self {
149        self.min_qty = Some(min_qty);
150        self
151    }
152
153    /// Set settlement type
154    pub fn with_settlement_type(mut self, settlement_type: char) -> Self {
155        self.settlement_type = Some(settlement_type);
156        self
157    }
158
159    /// Set custom label
160    pub fn with_label(mut self, label: String) -> Self {
161        self.deribit_label = Some(label);
162        self
163    }
164
165    /// Set market segment ID
166    pub fn with_market_segment_id(mut self, segment_id: String) -> Self {
167        self.market_segment_id = Some(segment_id);
168        self
169    }
170
171    /// Convert to FIX message
172    pub fn to_fix_message(
173        &self,
174        sender_comp_id: &str,
175        target_comp_id: &str,
176        msg_seq_num: u32,
177    ) -> DeribitFixResult<String> {
178        let mut builder = MessageBuilder::new()
179            .msg_type(MsgType::QuoteRequest)
180            .sender_comp_id(sender_comp_id.to_string())
181            .target_comp_id(target_comp_id.to_string())
182            .msg_seq_num(msg_seq_num)
183            .sending_time(Utc::now());
184
185        // Required fields
186        builder = builder
187            .field(131, self.quote_req_id.clone()) // QuoteReqID
188            .field(55, self.symbol.clone()) // Symbol
189            .field(537, i32::from(self.quote_type).to_string()) // QuoteType
190            .field(54, char::from(self.side).to_string()) // Side
191            .field(38, self.order_qty.to_string()); // OrderQty
192
193        // Optional fields
194        if let Some(valid_until_time) = &self.valid_until_time {
195            builder = builder.field(
196                62,
197                valid_until_time.format("%Y%m%d-%H:%M:%S%.3f").to_string(),
198            );
199        }
200
201        if let Some(quote_request_type) = &self.quote_request_type {
202            builder = builder.field(303, quote_request_type.to_string());
203        }
204
205        if let Some(time_in_force) = &self.time_in_force {
206            builder = builder.field(59, char::from(*time_in_force).to_string());
207        }
208
209        if let Some(min_qty) = &self.min_qty {
210            builder = builder.field(110, min_qty.to_string());
211        }
212
213        if let Some(settlement_type) = &self.settlement_type {
214            builder = builder.field(63, settlement_type.to_string());
215        }
216
217        if let Some(deribit_label) = &self.deribit_label {
218            builder = builder.field(100010, deribit_label.clone());
219        }
220
221        if let Some(market_segment_id) = &self.market_segment_id {
222            builder = builder.field(1300, market_segment_id.clone());
223        }
224
225        Ok(builder.build()?.to_string())
226    }
227}
228
229impl_json_display!(QuoteRequest);
230impl_json_debug_pretty!(QuoteRequest);
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_quote_request_creation() {
238        let request = QuoteRequest::new(
239            "QR123".to_string(),
240            "BTC-PERPETUAL".to_string(),
241            QuoteType::Tradeable,
242            OrderSide::Buy,
243            10.0,
244        );
245
246        assert_eq!(request.quote_req_id, "QR123");
247        assert_eq!(request.symbol, "BTC-PERPETUAL");
248        assert_eq!(request.quote_type, QuoteType::Tradeable);
249        assert_eq!(request.side, OrderSide::Buy);
250        assert_eq!(request.order_qty, 10.0);
251    }
252
253    #[test]
254    fn test_quote_request_tradeable() {
255        let request = QuoteRequest::tradeable(
256            "QR456".to_string(),
257            "ETH-PERPETUAL".to_string(),
258            OrderSide::Sell,
259            5.0,
260        );
261
262        assert_eq!(request.quote_type, QuoteType::Tradeable);
263        assert_eq!(request.side, OrderSide::Sell);
264        assert_eq!(request.order_qty, 5.0);
265    }
266
267    #[test]
268    fn test_quote_request_indicative() {
269        let request = QuoteRequest::indicative(
270            "QR789".to_string(),
271            "BTC-PERPETUAL".to_string(),
272            OrderSide::Buy,
273            15.0,
274        );
275
276        assert_eq!(request.quote_type, QuoteType::Indicative);
277        assert_eq!(request.order_qty, 15.0);
278    }
279
280    #[test]
281    fn test_quote_request_with_options() {
282        let valid_until = Utc::now() + chrono::Duration::hours(1);
283        let request = QuoteRequest::new(
284            "QR999".to_string(),
285            "ETH-PERPETUAL".to_string(),
286            QuoteType::RestrictedTradeable,
287            OrderSide::Buy,
288            20.0,
289        )
290        .with_valid_until(valid_until)
291        .with_quote_request_type(1)
292        .with_time_in_force(TimeInForce::GoodTillCancelled)
293        .with_min_qty(5.0)
294        .with_settlement_type('0')
295        .with_label("test-quote".to_string())
296        .with_market_segment_id("DERIBIT".to_string());
297
298        assert_eq!(request.valid_until_time, Some(valid_until));
299        assert_eq!(request.quote_request_type, Some(1));
300        assert_eq!(request.time_in_force, Some(TimeInForce::GoodTillCancelled));
301        assert_eq!(request.min_qty, Some(5.0));
302        assert_eq!(request.settlement_type, Some('0'));
303        assert_eq!(request.deribit_label, Some("test-quote".to_string()));
304        assert_eq!(request.market_segment_id, Some("DERIBIT".to_string()));
305    }
306
307    #[test]
308    fn test_quote_request_to_fix_message() {
309        let request = QuoteRequest::tradeable(
310            "QR123".to_string(),
311            "BTC-PERPETUAL".to_string(),
312            OrderSide::Buy,
313            10.0,
314        )
315        .with_label("test-label".to_string());
316
317        let fix_message = request.to_fix_message("SENDER", "TARGET", 1).unwrap();
318
319        // Check that the message contains required fields
320        assert!(fix_message.contains("35=R")); // MsgType
321        assert!(fix_message.contains("131=QR123")); // QuoteReqID
322        assert!(fix_message.contains("55=BTC-PERPETUAL")); // Symbol
323        assert!(fix_message.contains("537=1")); // QuoteType=Tradeable
324        assert!(fix_message.contains("54=1")); // Side=Buy
325        assert!(fix_message.contains("38=10")); // OrderQty
326        assert!(fix_message.contains("100010=test-label")); // Custom label
327    }
328
329    #[test]
330    fn test_quote_type_conversions() {
331        assert_eq!(i32::from(QuoteType::Indicative), 0);
332        assert_eq!(i32::from(QuoteType::Tradeable), 1);
333        assert_eq!(i32::from(QuoteType::RestrictedTradeable), 2);
334        assert_eq!(i32::from(QuoteType::Counter), 3);
335
336        assert_eq!(QuoteType::try_from(0).unwrap(), QuoteType::Indicative);
337        assert_eq!(QuoteType::try_from(1).unwrap(), QuoteType::Tradeable);
338        assert_eq!(
339            QuoteType::try_from(2).unwrap(),
340            QuoteType::RestrictedTradeable
341        );
342        assert_eq!(QuoteType::try_from(3).unwrap(), QuoteType::Counter);
343
344        assert!(QuoteType::try_from(99).is_err());
345    }
346}