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    QuoteAccepted {
191        #[serde(rename = "E")]
192        event_time: i64,
193        #[serde(rename = "R")]
194        rfq_id: u64,
195        #[serde(rename = "u")]
196        quote_id: u64,
197        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
198        client_id: Option<u32>,
199        #[serde(rename = "s")]
200        symbol: String,
201        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
202        price: Option<Decimal>,
203        #[serde(rename = "X")]
204        order_status: OrderStatus,
205        #[serde(rename = "T")]
206        timestamp: i64,
207    },
208    QuoteCancelled {
209        #[serde(rename = "E")]
210        event_time: i64,
211        #[serde(rename = "R")]
212        rfq_id: u64,
213        #[serde(rename = "u")]
214        quote_id: u64,
215        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
216        client_id: Option<u32>,
217        #[serde(rename = "s")]
218        symbol: String,
219        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
220        price: Option<Decimal>,
221        #[serde(rename = "X")]
222        order_status: OrderStatus,
223        #[serde(rename = "T")]
224        timestamp: i64,
225    },
226    RfqCandidate {
227        #[serde(rename = "E")]
228        event_time: i64,
229        #[serde(rename = "R")]
230        rfq_id: u64,
231        #[serde(rename = "u")]
232        quote_id: u64,
233        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
234        client_id: Option<u32>,
235        #[serde(rename = "s")]
236        symbol: String,
237        #[serde(rename = "S", skip_serializing_if = "Option::is_none")]
238        side: Option<Side>,
239        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
240        quantity: Option<Decimal>,
241        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
242        quote_quantity: Option<Decimal>,
243        #[serde(rename = "p")]
244        price: Decimal,
245        #[serde(rename = "X")]
246        order_status: OrderStatus,
247        #[serde(rename = "T")]
248        timestamp: i64,
249    },
250    RfqFilled {
251        #[serde(rename = "E")]
252        event_time: i64,
253        #[serde(rename = "R")]
254        rfq_id: u64,
255        #[serde(rename = "u")]
256        quote_id: u64,
257        #[serde(rename = "C", skip_serializing_if = "Option::is_none")]
258        client_id: Option<u32>,
259        #[serde(rename = "s")]
260        symbol: String,
261        #[serde(rename = "S")]
262        side: Side,
263        #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
264        quantity: Option<Decimal>,
265        #[serde(rename = "Q", skip_serializing_if = "Option::is_none")]
266        quote_quantity: Option<Decimal>,
267        #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
268        price: Option<Decimal>,
269        #[serde(rename = "X")]
270        order_status: OrderStatus,
271        #[serde(rename = "T")]
272        timestamp: i64,
273        #[serde(rename = "o", default)]
274        system_order_type: Option<SystemOrderType>,
275    },
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct Quote {
281    /// Unique RFQ order ID assigned by the matching engine.
282    pub rfq_id: String,
283
284    /// Unique RFQ quote ID, assigned by the matching engine.
285    pub quote_id: String,
286
287    /// Custom RFQ quote ID assigned by the maker (optionally)
288    pub client_id: Option<u32>,
289
290    /// Quote Bid Price.
291    pub bid_price: Decimal,
292
293    /// Quote Ask Price.
294    pub ask_price: Decimal,
295
296    /// Status.
297    pub status: OrderStatus,
298
299    /// Time the quote was created.
300    pub created_at: i64,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct RequestForQuote {
306    pub rfq_id: String,
307    pub client_id: Option<u32>,
308    pub symbol: String,
309    pub side: Side,
310    pub price: Option<Decimal>,
311    pub quantity: Option<Decimal>,
312    pub quote_quantity: Option<Decimal>,
313    pub submission_time: i64,
314    pub expiry_time: i64,
315    pub status: OrderStatus,
316    pub execution_mode: RfqExecutionMode,
317    pub created_at: i64,
318    #[serde(default)]
319    pub system_order_type: Option<SystemOrderType>,
320}
321
322impl QuotePayload {
323    pub fn new(rfq_id: String, bid_price: Decimal, ask_price: Decimal) -> Self {
324        Self {
325            rfq_id,
326            bid_price,
327            ask_price,
328            client_id: None,
329            auto_lend: None,
330            auto_lend_redeem: None,
331            auto_borrow: None,
332            auto_borrow_repay: None,
333        }
334    }
335
336    pub fn with_client_id(mut self, client_id: u32) -> Self {
337        self.client_id = Some(client_id);
338        self
339    }
340
341    pub fn with_auto_lend(mut self, auto_lend: bool) -> Self {
342        self.auto_lend = Some(auto_lend);
343        self
344    }
345
346    pub fn with_auto_lend_redeem(mut self, auto_lend_redeem: bool) -> Self {
347        self.auto_lend_redeem = Some(auto_lend_redeem);
348        self
349    }
350
351    pub fn with_auto_borrow(mut self, auto_borrow: bool) -> Self {
352        self.auto_borrow = Some(auto_borrow);
353        self
354    }
355
356    pub fn with_auto_borrow_repay(mut self, auto_borrow_repay: bool) -> Self {
357        self.auto_borrow_repay = Some(auto_borrow_repay);
358        self
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn rfq_active_without_system_order_type() {
368        let data = r#"{"e":"rfqActive","E":1234567890,"R":123,"s":"BTC_USDC","q":"1.5","w":1234567890,"W":1234567899,"X":"New","T":1234567890}"#;
369        let update: RequestForQuoteUpdate = serde_json::from_str(data).unwrap();
370        match update {
371            RequestForQuoteUpdate::RfqActive {
372                system_order_type, ..
373            } => {
374                assert!(system_order_type.is_none());
375            }
376            _ => panic!("Expected RfqActive"),
377        }
378    }
379
380    #[test]
381    fn rfq_active_with_system_order_type() {
382        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"}"#;
383        let update: RequestForQuoteUpdate = serde_json::from_str(data).unwrap();
384        match update {
385            RequestForQuoteUpdate::RfqActive {
386                system_order_type, ..
387            } => {
388                assert_eq!(
389                    system_order_type,
390                    Some(SystemOrderType::CollateralConversion)
391                );
392            }
393            _ => panic!("Expected RfqActive"),
394        }
395    }
396}