1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum QuoteType {
19 Indicative,
21 Tradeable,
23 RestrictedTradeable,
25 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#[derive(Clone, PartialEq, Serialize, Deserialize)]
56pub struct QuoteRequest {
57 pub quote_req_id: String,
59 pub symbol: String,
61 pub quote_type: QuoteType,
63 pub side: OrderSide,
65 pub order_qty: f64,
67 pub valid_until_time: Option<DateTime<Utc>>,
69 pub quote_request_type: Option<i32>,
71 pub time_in_force: Option<TimeInForce>,
73 pub min_qty: Option<f64>,
75 pub settlement_type: Option<char>,
77 pub deribit_label: Option<String>,
79 pub market_segment_id: Option<String>,
81 pub total_volume_traded: Option<f64>,
83 pub transact_time: Option<DateTime<Utc>>,
85}
86
87impl QuoteRequest {
88 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 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 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 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 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 pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
148 self.time_in_force = Some(tif);
149 self
150 }
151
152 pub fn with_min_qty(mut self, min_qty: f64) -> Self {
154 self.min_qty = Some(min_qty);
155 self
156 }
157
158 pub fn with_settlement_type(mut self, settlement_type: char) -> Self {
160 self.settlement_type = Some(settlement_type);
161 self
162 }
163
164 pub fn with_label(mut self, label: String) -> Self {
166 self.deribit_label = Some(label);
167 self
168 }
169
170 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 #[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 #[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 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 builder = builder
206 .field(131, self.quote_req_id.clone()) .field(55, self.symbol.clone()) .field(537, i32::from(self.quote_type).to_string()) .field(54, char::from(self.side).to_string()) .field(38, self.order_qty.to_string()); 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 assert!(fix_message.contains("35=R")); assert!(fix_message.contains("131=QR123")); assert!(fix_message.contains("55=BTC-PERPETUAL")); assert!(fix_message.contains("537=1")); assert!(fix_message.contains("54=1")); assert!(fix_message.contains("38=10")); assert!(fix_message.contains("100010=test-label")); }
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 assert!(fix_message.contains("387=500.25")); assert!(fix_message.contains("60=")); }
408}