Skip to main content

bpx_api_types/
rfq.rs

1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3use strum::{Display, EnumString};
4
5use crate::order::{OrderStatus, Side, SystemOrderType};
6
7#[derive(
8    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
9)]
10#[strum(serialize_all = "PascalCase")]
11#[serde(rename_all = "PascalCase")]
12pub enum RfqExecutionMode {
13    #[default]
14    AwaitAccept,
15    Immediate,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct RequestForQuotePayload {
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub client_id: Option<u32>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub quantity: Option<Decimal>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub quote_quantity: Option<Decimal>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub price: Option<Decimal>,
29    pub symbol: String,
30    pub side: Side,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub execution_mode: Option<RfqExecutionMode>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct RequestForQuoteCancelPayload {
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub rfq_id: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub client_id: Option<u32>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct RequestForQuoteRefreshPayload {
47    pub rfq_id: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct QuoteAcceptPayload {
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub rfq_id: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub client_id: Option<u32>,
57    pub quote_id: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct QuotePayload {
63    pub rfq_id: String,
64    pub bid_price: Decimal,
65    pub ask_price: Decimal,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub client_id: Option<u32>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub auto_lend: Option<bool>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub auto_lend_redeem: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub auto_borrow: Option<bool>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub auto_borrow_repay: Option<bool>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct RequestForQuoteStream {
80    pub stream: String,
81    pub data: RequestForQuoteUpdate,
82}
83
84/// RequestForQuote updates received from the websocket.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "e", rename_all = "camelCase")] // Discriminates based on "e" field
87pub enum RequestForQuoteUpdate {
88    RfqActive {
89        #[serde(rename = "E")]
90        event_time: i64,
91        #[serde(rename = "R")]
92        rfq_id: u64,
93        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
94        client_id: Option<u32>,
95        #[serde(rename = "s")]
96        symbol: String,
97        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
98        quantity: Option<Decimal>,
99        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
100        quote_quantity: Option<Decimal>,
101        #[serde(rename = "w")]
102        submission_time: i64,
103        #[serde(rename = "W")]
104        expiry_time: i64,
105        #[serde(rename = "X")]
106        order_status: OrderStatus,
107        #[serde(rename = "T")]
108        timestamp: i64,
109        #[serde(rename = "o", default)]
110        system_order_type: Option<SystemOrderType>,
111    },
112    RfqRefreshed {
113        #[serde(rename = "E")]
114        event_time: i64,
115        #[serde(rename = "R")]
116        rfq_id: u64,
117        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
118        client_id: Option<u32>,
119        #[serde(rename = "s")]
120        symbol: String,
121        #[serde(rename = "S")]
122        side: Side,
123        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
124        quantity: Option<Decimal>,
125        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
126        quote_quantity: Option<Decimal>,
127        #[serde(rename = "w")]
128        submission_time: i64,
129        #[serde(rename = "W")]
130        expiry_time: i64,
131        #[serde(rename = "X")]
132        order_status: OrderStatus,
133        #[serde(rename = "T")]
134        timestamp: i64,
135        #[serde(rename = "o", default)]
136        system_order_type: Option<SystemOrderType>,
137    },
138    RfqAccepted {
139        #[serde(rename = "E")]
140        event_time: i64,
141        #[serde(rename = "R")]
142        rfq_id: u64,
143        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
144        client_id: Option<u32>,
145        #[serde(rename = "s")]
146        symbol: String,
147        #[serde(rename = "S")]
148        side: Side,
149        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
150        quantity: Option<Decimal>,
151        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
152        quote_quantity: Option<Decimal>,
153        #[serde(rename = "w")]
154        submission_time: i64,
155        #[serde(rename = "W")]
156        expiry_time: i64,
157        #[serde(rename = "X")]
158        order_status: OrderStatus,
159        #[serde(rename = "T")]
160        timestamp: i64,
161        #[serde(rename = "o", default)]
162        system_order_type: Option<SystemOrderType>,
163    },
164    RfqCancelled {
165        #[serde(rename = "E")]
166        event_time: i64,
167        #[serde(rename = "R")]
168        rfq_id: u64,
169        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
170        client_id: Option<u32>,
171        #[serde(rename = "s")]
172        symbol: String,
173        #[serde(rename = "S")]
174        side: Side,
175        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
176        quantity: Option<Decimal>,
177        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
178        quote_quantity: Option<Decimal>,
179        #[serde(rename = "w")]
180        submission_time: i64,
181        #[serde(rename = "W")]
182        expiry_time: i64,
183        #[serde(rename = "X")]
184        order_status: OrderStatus,
185        #[serde(rename = "T")]
186        timestamp: i64,
187        #[serde(rename = "o", default)]
188        system_order_type: Option<SystemOrderType>,
189    },
190    // Sent to quoters. Deferred settlement RFQs only.
191    RfqAcceptedBinding {
192        #[serde(rename = "E")]
193        event_time: i64,
194        #[serde(rename = "R")]
195        rfq_id: u64,
196        #[serde(rename = "u")]
197        quote_id: u64,
198        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
199        client_id: Option<u32>,
200        #[serde(rename = "s")]
201        symbol: String,
202        /// Side of the taker.
203        #[serde(rename = "S", skip_serializing_if = "Option::is_none")]
204        side: Option<Side>,
205        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
206        quantity: Option<Decimal>,
207        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
208        quote_quantity: Option<Decimal>,
209        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
210        price: Option<Decimal>,
211        #[serde(rename = "X")]
212        order_status: OrderStatus,
213        #[serde(rename = "T")]
214        timestamp: i64,
215    },
216    QuoteAccepted {
217        #[serde(rename = "E")]
218        event_time: i64,
219        #[serde(rename = "R")]
220        rfq_id: u64,
221        #[serde(rename = "u")]
222        quote_id: u64,
223        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
224        client_id: Option<u32>,
225        #[serde(rename = "s")]
226        symbol: String,
227        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
228        price: Option<Decimal>,
229        #[serde(rename = "X")]
230        order_status: OrderStatus,
231        #[serde(rename = "T")]
232        timestamp: i64,
233    },
234    QuoteCancelled {
235        #[serde(rename = "E")]
236        event_time: i64,
237        #[serde(rename = "R")]
238        rfq_id: u64,
239        #[serde(rename = "u")]
240        quote_id: u64,
241        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
242        client_id: Option<u32>,
243        #[serde(rename = "s")]
244        symbol: String,
245        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
246        price: Option<Decimal>,
247        #[serde(rename = "X")]
248        order_status: OrderStatus,
249        #[serde(rename = "T")]
250        timestamp: i64,
251    },
252    RfqCandidate {
253        #[serde(rename = "E")]
254        event_time: i64,
255        #[serde(rename = "R")]
256        rfq_id: u64,
257        #[serde(rename = "u")]
258        quote_id: u64,
259        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
260        client_id: Option<u32>,
261        #[serde(rename = "s")]
262        symbol: String,
263        #[serde(rename = "S", skip_serializing_if = "Option::is_none")]
264        side: Option<Side>,
265        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
266        quantity: Option<Decimal>,
267        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
268        quote_quantity: Option<Decimal>,
269        #[serde(rename = "p")]
270        price: Decimal,
271        #[serde(rename = "X")]
272        order_status: OrderStatus,
273        #[serde(rename = "T")]
274        timestamp: i64,
275    },
276    RfqFilled {
277        #[serde(rename = "E")]
278        event_time: i64,
279        #[serde(rename = "R")]
280        rfq_id: u64,
281        #[serde(rename = "u")]
282        quote_id: u64,
283        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
284        client_id: Option<u32>,
285        #[serde(rename = "s")]
286        symbol: String,
287        #[serde(rename = "S")]
288        side: Side,
289        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
290        quantity: Option<Decimal>,
291        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
292        quote_quantity: Option<Decimal>,
293        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
294        price: Option<Decimal>,
295        #[serde(rename = "X")]
296        order_status: OrderStatus,
297        #[serde(rename = "T")]
298        timestamp: i64,
299        #[serde(rename = "o", default)]
300        system_order_type: Option<SystemOrderType>,
301    },
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct Quote {
307    /// Unique RFQ order ID assigned by the matching engine.
308    pub rfq_id: String,
309
310    /// Unique RFQ quote ID, assigned by the matching engine.
311    pub quote_id: String,
312
313    /// Custom RFQ quote ID assigned by the maker (optionally)
314    pub client_id: Option<u32>,
315
316    /// Quote Bid Price.
317    pub bid_price: Decimal,
318
319    /// Quote Ask Price.
320    pub ask_price: Decimal,
321
322    /// Status.
323    pub status: OrderStatus,
324
325    /// Time the quote was created.
326    pub created_at: i64,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct RequestForQuote {
332    pub rfq_id: String,
333    pub client_id: Option<u32>,
334    pub symbol: String,
335    pub side: Side,
336    pub price: Option<Decimal>,
337    pub quantity: Option<Decimal>,
338    pub quote_quantity: Option<Decimal>,
339    pub submission_time: i64,
340    pub expiry_time: i64,
341    pub status: OrderStatus,
342    pub execution_mode: RfqExecutionMode,
343    pub created_at: i64,
344    #[serde(default)]
345    pub system_order_type: Option<SystemOrderType>,
346}
347
348impl QuotePayload {
349    pub fn new(rfq_id: String, bid_price: Decimal, ask_price: Decimal) -> Self {
350        Self {
351            rfq_id,
352            bid_price,
353            ask_price,
354            client_id: None,
355            auto_lend: None,
356            auto_lend_redeem: None,
357            auto_borrow: None,
358            auto_borrow_repay: None,
359        }
360    }
361
362    pub fn with_client_id(mut self, client_id: u32) -> Self {
363        self.client_id = Some(client_id);
364        self
365    }
366
367    pub fn with_auto_lend(mut self, auto_lend: bool) -> Self {
368        self.auto_lend = Some(auto_lend);
369        self
370    }
371
372    pub fn with_auto_lend_redeem(mut self, auto_lend_redeem: bool) -> Self {
373        self.auto_lend_redeem = Some(auto_lend_redeem);
374        self
375    }
376
377    pub fn with_auto_borrow(mut self, auto_borrow: bool) -> Self {
378        self.auto_borrow = Some(auto_borrow);
379        self
380    }
381
382    pub fn with_auto_borrow_repay(mut self, auto_borrow_repay: bool) -> Self {
383        self.auto_borrow_repay = Some(auto_borrow_repay);
384        self
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn rfq_active_without_system_order_type() {
394        let data = r#"{"e":"rfqActive","E":1234567890,"R":123,"s":"BTC_USDC","q":"1.5","w":1234567890,"W":1234567899,"X":"New","T":1234567890}"#;
395        let update: RequestForQuoteUpdate = serde_json::from_str(data).unwrap();
396        match update {
397            RequestForQuoteUpdate::RfqActive {
398                system_order_type, ..
399            } => {
400                assert!(system_order_type.is_none());
401            }
402            _ => panic!("Expected RfqActive"),
403        }
404    }
405
406    #[test]
407    fn rfq_active_with_system_order_type() {
408        let data = r#"{"e":"rfqActive","E":1234567890,"R":123,"s":"BTC_USDC","q":"1.5","w":1234567890,"W":1234567899,"X":"New","T":1234567890,"o":"CollateralConversion"}"#;
409        let update: RequestForQuoteUpdate = serde_json::from_str(data).unwrap();
410        match update {
411            RequestForQuoteUpdate::RfqActive {
412                system_order_type, ..
413            } => {
414                assert_eq!(
415                    system_order_type,
416                    Some(SystemOrderType::CollateralConversion)
417                );
418            }
419            _ => panic!("Expected RfqActive"),
420        }
421    }
422}