Skip to main content

deribit_http/model/response/
block_rfq.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 7/3/26
5******************************************************************************/
6//! Block RFQ response models for Request for Quote workflow.
7
8use crate::model::types::Direction;
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12/// State of a Block RFQ
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum BlockRfqState {
16    /// Block RFQ is open and accepting quotes
17    #[default]
18    Open,
19    /// Block RFQ has been filled
20    Filled,
21    /// Block RFQ has been traded
22    Traded,
23    /// Block RFQ has been cancelled
24    Cancelled,
25    /// Block RFQ has expired
26    Expired,
27    /// Block RFQ is closed
28    Closed,
29    /// Block RFQ was created
30    Created,
31}
32
33/// Role of user in Block RFQ
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
35#[serde(rename_all = "snake_case")]
36pub enum BlockRfqRole {
37    /// User is the taker (creator) of the RFQ
38    #[default]
39    Taker,
40    /// User is a maker (quoter)
41    Maker,
42    /// Any role
43    Any,
44}
45
46/// State of a Block RFQ quote
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum QuoteState {
50    /// Quote is open
51    #[default]
52    Open,
53    /// Quote has been filled
54    Filled,
55    /// Quote has been cancelled
56    Cancelled,
57}
58
59/// Execution instruction for quotes
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
61#[serde(rename_all = "snake_case")]
62pub enum ExecutionInstruction {
63    /// Quote can only be filled entirely or not at all
64    AllOrNone,
65    /// Quote can be filled partially or fully
66    #[default]
67    AnyPartOf,
68}
69
70/// Time in force for accepting Block RFQ
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
72#[serde(rename_all = "snake_case")]
73pub enum BlockRfqTimeInForce {
74    /// Fill immediately or cancel
75    #[default]
76    FillOrKill,
77    /// Good until cancelled
78    GoodTilCancelled,
79}
80
81/// Leg of a Block RFQ
82#[skip_serializing_none]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct BlockRfqLeg {
85    /// Instrument name (e.g., "BTC-PERPETUAL")
86    pub instrument_name: String,
87    /// Direction of the leg
88    pub direction: Direction,
89    /// Ratio of amount between legs
90    #[serde(default)]
91    pub ratio: Option<f64>,
92    /// Amount for the leg (used in create_block_rfq)
93    #[serde(default)]
94    pub amount: Option<f64>,
95    /// Price for the leg (used in quotes)
96    #[serde(default)]
97    pub price: Option<f64>,
98}
99
100/// Hedge leg of a Block RFQ
101#[skip_serializing_none]
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BlockRfqHedge {
104    /// Instrument name (e.g., "BTC-PERPETUAL")
105    pub instrument_name: String,
106    /// Direction of the hedge
107    pub direction: Direction,
108    /// Price for the hedge
109    pub price: f64,
110    /// Amount for the hedge
111    pub amount: f64,
112}
113
114/// Bid/Ask quote in Block RFQ
115#[skip_serializing_none]
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct BlockRfqBidAsk {
118    /// Maker of the quote
119    #[serde(default)]
120    pub maker: Option<String>,
121    /// Price of the quote
122    pub price: f64,
123    /// Timestamp of last update (milliseconds since Unix epoch)
124    #[serde(default)]
125    pub last_update_timestamp: Option<i64>,
126    /// Execution instruction
127    #[serde(default)]
128    pub execution_instruction: Option<ExecutionInstruction>,
129    /// Amount of the quote
130    #[serde(default)]
131    pub amount: Option<f64>,
132}
133
134/// Trade allocation for Block RFQ pre-allocation
135#[skip_serializing_none]
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct BlockRfqTradeAllocation {
138    /// User ID (subaccount or main account)
139    #[serde(default)]
140    pub user_id: Option<i64>,
141    /// Client info for broker allocation
142    #[serde(default)]
143    pub client_info: Option<BlockRfqClientInfo>,
144    /// Amount allocated
145    pub amount: f64,
146}
147
148/// Client info for broker allocation
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct BlockRfqClientInfo {
151    /// Client ID
152    pub client_id: String,
153    /// User ID within client
154    #[serde(default)]
155    pub user_id: Option<String>,
156}
157
158/// Index prices in Block RFQ trade
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct IndexPrices {
161    /// BTC/USD index price
162    #[serde(default)]
163    pub btc_usd: Option<f64>,
164    /// BTC/USDC index price
165    #[serde(default)]
166    pub btc_usdc: Option<f64>,
167    /// ETH/USD index price
168    #[serde(default)]
169    pub eth_usd: Option<f64>,
170    /// ETH/USDC index price
171    #[serde(default)]
172    pub eth_usdc: Option<f64>,
173}
174
175/// Block RFQ representation
176#[skip_serializing_none]
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct BlockRfq {
179    /// Block RFQ ID
180    pub block_rfq_id: i64,
181    /// State of the Block RFQ
182    pub state: BlockRfqState,
183    /// Role of the user
184    pub role: BlockRfqRole,
185    /// Total amount
186    pub amount: f64,
187    /// Minimum amount for trading
188    #[serde(default)]
189    pub min_trade_amount: Option<f64>,
190    /// Combo ID
191    #[serde(default)]
192    pub combo_id: Option<String>,
193    /// List of legs
194    pub legs: Vec<BlockRfqLeg>,
195    /// Hedge leg (optional)
196    #[serde(default)]
197    pub hedge: Option<BlockRfqHedge>,
198    /// Creation timestamp (milliseconds since Unix epoch)
199    pub creation_timestamp: i64,
200    /// Expiration timestamp (milliseconds since Unix epoch)
201    pub expiration_timestamp: i64,
202    /// User-defined label
203    #[serde(default)]
204    pub label: Option<String>,
205    /// List of targeted makers
206    #[serde(default)]
207    pub makers: Option<Vec<String>>,
208    /// Taker rating
209    #[serde(default)]
210    pub taker_rating: Option<String>,
211    /// Bid quotes
212    #[serde(default)]
213    pub bids: Option<Vec<BlockRfqBidAsk>>,
214    /// Ask quotes
215    #[serde(default)]
216    pub asks: Option<Vec<BlockRfqBidAsk>>,
217    /// Mark price (for filled RFQs)
218    #[serde(default)]
219    pub mark_price: Option<f64>,
220    /// Trades (for filled RFQs)
221    #[serde(default)]
222    pub trades: Option<Vec<BlockRfqTradeInfo>>,
223}
224
225impl BlockRfq {
226    /// Returns `true` if the Block RFQ is open
227    #[must_use]
228    pub fn is_open(&self) -> bool {
229        self.state == BlockRfqState::Open
230    }
231
232    /// Returns `true` if the Block RFQ is filled
233    #[must_use]
234    pub fn is_filled(&self) -> bool {
235        self.state == BlockRfqState::Filled
236    }
237
238    /// Returns `true` if the Block RFQ is cancelled
239    #[must_use]
240    pub fn is_cancelled(&self) -> bool {
241        self.state == BlockRfqState::Cancelled
242    }
243
244    /// Returns `true` if the user is the taker
245    #[must_use]
246    pub fn is_taker(&self) -> bool {
247        self.role == BlockRfqRole::Taker
248    }
249
250    /// Returns `true` if the user is a maker
251    #[must_use]
252    pub fn is_maker(&self) -> bool {
253        self.role == BlockRfqRole::Maker
254    }
255}
256
257/// Trade info within a Block RFQ
258#[skip_serializing_none]
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct BlockRfqTradeInfo {
261    /// Trade price
262    pub price: f64,
263    /// Trade amount
264    pub amount: f64,
265    /// Trade direction
266    pub direction: Direction,
267    /// Hedge amount (optional)
268    #[serde(default)]
269    pub hedge_amount: Option<f64>,
270}
271
272/// Block RFQ quote response
273#[skip_serializing_none]
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct BlockRfqQuote {
276    /// Quote ID
277    pub block_rfq_quote_id: i64,
278    /// Block RFQ ID
279    pub block_rfq_id: i64,
280    /// Quote state
281    pub quote_state: QuoteState,
282    /// Price
283    pub price: f64,
284    /// Amount
285    pub amount: f64,
286    /// Direction
287    pub direction: Direction,
288    /// Filled amount
289    #[serde(default)]
290    pub filled_amount: Option<f64>,
291    /// Legs
292    pub legs: Vec<BlockRfqLeg>,
293    /// Hedge (optional)
294    #[serde(default)]
295    pub hedge: Option<BlockRfqHedge>,
296    /// Execution instruction
297    #[serde(default)]
298    pub execution_instruction: Option<ExecutionInstruction>,
299    /// Creation timestamp (milliseconds since Unix epoch)
300    pub creation_timestamp: i64,
301    /// Last update timestamp (milliseconds since Unix epoch)
302    pub last_update_timestamp: i64,
303    /// Whether the quote was replaced/edited
304    #[serde(default)]
305    pub replaced: Option<bool>,
306    /// User-defined label
307    #[serde(default)]
308    pub label: Option<String>,
309    /// Application name
310    #[serde(default)]
311    pub app_name: Option<String>,
312    /// Cancellation reason
313    #[serde(default)]
314    pub cancel_reason: Option<String>,
315}
316
317impl BlockRfqQuote {
318    /// Returns `true` if the quote is open
319    #[must_use]
320    pub fn is_open(&self) -> bool {
321        self.quote_state == QuoteState::Open
322    }
323
324    /// Returns `true` if the quote is filled
325    #[must_use]
326    pub fn is_filled(&self) -> bool {
327        self.quote_state == QuoteState::Filled
328    }
329
330    /// Returns `true` if the quote is cancelled
331    #[must_use]
332    pub fn is_cancelled(&self) -> bool {
333        self.quote_state == QuoteState::Cancelled
334    }
335}
336
337/// Public Block RFQ trade (from get_block_rfq_trades)
338#[skip_serializing_none]
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct BlockRfqPublicTrade {
341    /// Block RFQ ID
342    pub id: i64,
343    /// Timestamp (milliseconds since Unix epoch)
344    pub timestamp: i64,
345    /// Combo ID
346    #[serde(default)]
347    pub combo_id: Option<String>,
348    /// Legs
349    pub legs: Vec<BlockRfqLeg>,
350    /// Amount
351    pub amount: f64,
352    /// Direction
353    pub direction: Direction,
354    /// Mark price
355    #[serde(default)]
356    pub mark_price: Option<f64>,
357    /// Trades
358    #[serde(default)]
359    pub trades: Option<Vec<BlockRfqTradeInfo>>,
360    /// Hedge (optional)
361    #[serde(default)]
362    pub hedge: Option<BlockRfqHedge>,
363    /// Index prices
364    #[serde(default)]
365    pub index_prices: Option<IndexPrices>,
366}
367
368/// Response for get_block_rfq_trades
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct BlockRfqTradesResponse {
371    /// Continuation token for pagination
372    #[serde(default)]
373    pub continuation: Option<String>,
374    /// List of Block RFQ trades
375    pub block_rfqs: Vec<BlockRfqPublicTrade>,
376}
377
378impl BlockRfqTradesResponse {
379    /// Returns `true` if there are no trades
380    #[must_use]
381    pub fn is_empty(&self) -> bool {
382        self.block_rfqs.is_empty()
383    }
384
385    /// Returns the number of trades
386    #[must_use]
387    pub fn len(&self) -> usize {
388        self.block_rfqs.len()
389    }
390
391    /// Returns `true` if there are more results
392    #[must_use]
393    pub fn has_more(&self) -> bool {
394        self.continuation.is_some()
395    }
396}
397
398/// Response for get_block_rfqs
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct BlockRfqsResponse {
401    /// Continuation token for pagination
402    #[serde(default)]
403    pub continuation: Option<String>,
404    /// List of Block RFQs
405    pub block_rfqs: Vec<BlockRfq>,
406}
407
408impl BlockRfqsResponse {
409    /// Returns `true` if there are no RFQs
410    #[must_use]
411    pub fn is_empty(&self) -> bool {
412        self.block_rfqs.is_empty()
413    }
414
415    /// Returns the number of RFQs
416    #[must_use]
417    pub fn len(&self) -> usize {
418        self.block_rfqs.len()
419    }
420
421    /// Returns `true` if there are more results
422    #[must_use]
423    pub fn has_more(&self) -> bool {
424        self.continuation.is_some()
425    }
426}
427
428/// Individual trade in accept_block_rfq response
429#[skip_serializing_none]
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct BlockRfqAcceptTrade {
432    /// Trade ID
433    pub trade_id: String,
434    /// Trade sequence number
435    #[serde(default)]
436    pub trade_seq: Option<i64>,
437    /// Instrument name
438    pub instrument_name: String,
439    /// Timestamp (milliseconds since Unix epoch)
440    pub timestamp: i64,
441    /// Trade state
442    pub state: String,
443    /// Fee
444    #[serde(default)]
445    pub fee: Option<f64>,
446    /// Fee currency
447    #[serde(default)]
448    pub fee_currency: Option<String>,
449    /// Amount
450    pub amount: f64,
451    /// Direction
452    pub direction: Direction,
453    /// Price
454    pub price: f64,
455    /// Index price
456    #[serde(default)]
457    pub index_price: Option<f64>,
458    /// Mark price
459    #[serde(default)]
460    pub mark_price: Option<f64>,
461    /// Profit/loss
462    #[serde(default)]
463    pub profit_loss: Option<f64>,
464    /// Order ID
465    #[serde(default)]
466    pub order_id: Option<String>,
467    /// Order type
468    #[serde(default)]
469    pub order_type: Option<String>,
470    /// Tick direction
471    #[serde(default)]
472    pub tick_direction: Option<i32>,
473    /// Combo ID
474    #[serde(default)]
475    pub combo_id: Option<String>,
476    /// Block RFQ ID
477    #[serde(default)]
478    pub block_rfq_id: Option<i64>,
479    /// Block trade ID
480    #[serde(default)]
481    pub block_trade_id: Option<String>,
482    /// Block trade leg count
483    #[serde(default)]
484    pub block_trade_leg_count: Option<i32>,
485    /// Whether API was used
486    #[serde(default)]
487    pub api: Option<bool>,
488    /// Number of contracts
489    #[serde(default)]
490    pub contracts: Option<f64>,
491    /// Post only flag
492    #[serde(default)]
493    pub post_only: Option<bool>,
494    /// MMP flag
495    #[serde(default)]
496    pub mmp: Option<bool>,
497    /// Risk reducing flag
498    #[serde(default)]
499    pub risk_reducing: Option<bool>,
500    /// Reduce only flag
501    #[serde(default)]
502    pub reduce_only: Option<bool>,
503    /// Self trade flag
504    #[serde(default)]
505    pub self_trade: Option<bool>,
506    /// Liquidity indicator (T=taker, M=maker)
507    #[serde(default)]
508    pub liquidity: Option<String>,
509    /// Matching ID
510    #[serde(default)]
511    pub matching_id: Option<String>,
512}
513
514/// Block trade in accept_block_rfq response
515#[derive(Debug, Clone, Serialize, Deserialize)]
516pub struct BlockRfqAcceptBlockTrade {
517    /// Block trade ID
518    pub id: String,
519    /// Timestamp (milliseconds since Unix epoch)
520    pub timestamp: i64,
521    /// Individual trades
522    pub trades: Vec<BlockRfqAcceptTrade>,
523}
524
525/// Response for accept_block_rfq
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct AcceptBlockRfqResponse {
528    /// Block trades
529    pub block_trades: Vec<BlockRfqAcceptBlockTrade>,
530}
531
532impl AcceptBlockRfqResponse {
533    /// Returns the total number of trades across all block trades
534    #[must_use]
535    pub fn total_trades(&self) -> usize {
536        self.block_trades.iter().map(|bt| bt.trades.len()).sum()
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_block_rfq_state_deserialization() {
546        let json = r#""open""#;
547        let state: BlockRfqState = serde_json::from_str(json).unwrap();
548        assert_eq!(state, BlockRfqState::Open);
549
550        let json = r#""cancelled""#;
551        let state: BlockRfqState = serde_json::from_str(json).unwrap();
552        assert_eq!(state, BlockRfqState::Cancelled);
553    }
554
555    #[test]
556    fn test_block_rfq_role_deserialization() {
557        let json = r#""taker""#;
558        let role: BlockRfqRole = serde_json::from_str(json).unwrap();
559        assert_eq!(role, BlockRfqRole::Taker);
560
561        let json = r#""maker""#;
562        let role: BlockRfqRole = serde_json::from_str(json).unwrap();
563        assert_eq!(role, BlockRfqRole::Maker);
564    }
565
566    #[test]
567    fn test_block_rfq_leg_deserialization() {
568        let json = r#"{
569            "instrument_name": "BTC-PERPETUAL",
570            "direction": "buy",
571            "ratio": 1,
572            "price": 70000
573        }"#;
574
575        let leg: BlockRfqLeg = serde_json::from_str(json).unwrap();
576        assert_eq!(leg.instrument_name, "BTC-PERPETUAL");
577        assert!(matches!(leg.direction, Direction::Buy));
578        assert_eq!(leg.ratio, Some(1.0));
579        assert_eq!(leg.price, Some(70000.0));
580    }
581
582    #[test]
583    fn test_block_rfq_deserialization() {
584        let json = r#"{
585            "block_rfq_id": 507,
586            "state": "created",
587            "role": "taker",
588            "amount": 20000,
589            "combo_id": "BTC-15NOV24",
590            "legs": [
591                {
592                    "direction": "sell",
593                    "instrument_name": "BTC-15NOV24",
594                    "ratio": 1
595                }
596            ],
597            "creation_timestamp": 1731062187555,
598            "expiration_timestamp": 1731062487555,
599            "bids": [],
600            "asks": [],
601            "makers": ["MAKER1"]
602        }"#;
603
604        let rfq: BlockRfq = serde_json::from_str(json).unwrap();
605        assert_eq!(rfq.block_rfq_id, 507);
606        assert_eq!(rfq.state, BlockRfqState::Created);
607        assert!(rfq.is_taker());
608        assert_eq!(rfq.amount, 20000.0);
609        assert_eq!(rfq.legs.len(), 1);
610    }
611
612    #[test]
613    fn test_block_rfq_quote_deserialization() {
614        let json = r#"{
615            "block_rfq_quote_id": 8,
616            "block_rfq_id": 3,
617            "quote_state": "open",
618            "price": 69600,
619            "amount": 10000,
620            "direction": "buy",
621            "legs": [
622                {
623                    "direction": "buy",
624                    "price": 69600,
625                    "instrument_name": "BTC-15NOV24",
626                    "ratio": 1
627                }
628            ],
629            "creation_timestamp": 1731076586371,
630            "last_update_timestamp": 1731076586371,
631            "replaced": false,
632            "filled_amount": 0,
633            "execution_instruction": "all_or_none"
634        }"#;
635
636        let quote: BlockRfqQuote = serde_json::from_str(json).unwrap();
637        assert_eq!(quote.block_rfq_quote_id, 8);
638        assert!(quote.is_open());
639        assert_eq!(
640            quote.execution_instruction,
641            Some(ExecutionInstruction::AllOrNone)
642        );
643    }
644
645    #[test]
646    fn test_block_rfq_trades_response_deserialization() {
647        let json = r#"{
648            "continuation": "1739739009234:6570",
649            "block_rfqs": [
650                {
651                    "id": 6611,
652                    "timestamp": 1739803305362,
653                    "combo_id": "BTC-CS-28FEB25-100000_106000",
654                    "legs": [
655                        {
656                            "price": 0.1,
657                            "direction": "buy",
658                            "instrument_name": "BTC-28FEB25-100000-C",
659                            "ratio": 1
660                        }
661                    ],
662                    "amount": 12.5,
663                    "direction": "sell",
664                    "mark_price": 0.010356754
665                }
666            ]
667        }"#;
668
669        let response: BlockRfqTradesResponse = serde_json::from_str(json).unwrap();
670        assert!(response.has_more());
671        assert_eq!(response.len(), 1);
672        assert_eq!(response.block_rfqs[0].id, 6611);
673    }
674}