Skip to main content

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