bpx_api_types/
order.rs

1use std::{fmt, str::FromStr};
2
3use rust_decimal::{prelude::FromPrimitive, Decimal};
4use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
5use strum::{Display, EnumString};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum TriggerBy {
9    LastPrice,
10    MarkPrice,
11    IndexPrice,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum TriggerQuantity {
16    Percent(Decimal),
17    Amount(Decimal),
18}
19
20impl<'de> Deserialize<'de> for TriggerQuantity {
21    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
22    where
23        D: Deserializer<'de>,
24    {
25        struct QtyVisitor;
26
27        impl Visitor<'_> for QtyVisitor {
28            type Value = TriggerQuantity;
29
30            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
31                f.write_str(r#"a string like "12.5%" or "0.01", or a number"#)
32            }
33
34            // ---------- JSON string ----------
35            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
36            where
37                E: serde::de::Error,
38            {
39                parse_str(v).map_err(serde::de::Error::custom)
40            }
41
42            // ---------- JSON numbers ----------
43            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
44            where
45                E: serde::de::Error,
46            {
47                Decimal::from_f64(v)
48                    .ok_or_else(|| serde::de::Error::custom("not a finite number"))
49                    .map(TriggerQuantity::Amount)
50            }
51
52            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
53            where
54                E: serde::de::Error,
55            {
56                Ok(TriggerQuantity::Amount(Decimal::from(v)))
57            }
58
59            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
60            where
61                E: serde::de::Error,
62            {
63                Ok(TriggerQuantity::Amount(Decimal::from(v)))
64            }
65        }
66
67        deserializer.deserialize_any(QtyVisitor)
68    }
69}
70
71impl Serialize for TriggerQuantity {
72    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
73    where
74        S: serde::Serializer,
75    {
76        serializer.serialize_str(
77            match self {
78                Self::Percent(percent) => format!("{percent}%"),
79                Self::Amount(amount) => format!("{amount}"),
80            }
81            .as_str(),
82        )
83    }
84}
85
86fn parse_str(s: &str) -> Result<TriggerQuantity, &'static str> {
87    if let Some(num) = s.strip_suffix('%') {
88        let d = Decimal::from_str(num.trim()).map_err(|_| "invalid percent value")?;
89        Ok(TriggerQuantity::Percent(d))
90    } else {
91        let d = Decimal::from_str(s.trim()).map_err(|_| "invalid decimal value")?;
92        Ok(TriggerQuantity::Amount(d))
93    }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct MarketOrder {
99    pub id: String,
100    pub client_id: Option<u32>,
101    pub symbol: String,
102    pub side: Side,
103    pub quantity: Option<Decimal>,
104    pub executed_quantity: Decimal,
105    pub quote_quantity: Option<Decimal>,
106    pub executed_quote_quantity: Decimal,
107    pub stop_loss_trigger_price: Option<Decimal>,
108    pub stop_loss_limit_price: Option<Decimal>,
109    pub stop_loss_trigger_by: Option<TriggerBy>,
110    pub take_profit_trigger_price: Option<Decimal>,
111    pub take_profit_limit_price: Option<Decimal>,
112    pub take_profit_trigger_by: Option<TriggerBy>,
113    pub trigger_by: Option<TriggerBy>,
114    pub trigger_price: Option<Decimal>,
115    pub trigger_quantity: Option<TriggerQuantity>,
116    pub triggered_at: Option<i64>,
117    pub time_in_force: TimeInForce,
118    pub related_order_id: Option<String>,
119    pub self_trade_prevention: SelfTradePrevention,
120    pub reduce_only: Option<bool>,
121    pub status: OrderStatus,
122    pub created_at: i64,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct LimitOrder {
128    pub id: String,
129    pub client_id: Option<u32>,
130    pub symbol: String,
131    pub side: Side,
132    pub quantity: Decimal,
133    pub executed_quantity: Decimal,
134    pub executed_quote_quantity: Decimal,
135    pub stop_loss_trigger_price: Option<Decimal>,
136    pub stop_loss_limit_price: Option<Decimal>,
137    pub stop_loss_trigger_by: Option<TriggerBy>,
138    pub take_profit_trigger_price: Option<Decimal>,
139    pub take_profit_limit_price: Option<Decimal>,
140    pub take_profit_trigger_by: Option<TriggerBy>,
141    pub price: Decimal,
142    pub trigger_by: Option<TriggerBy>,
143    pub trigger_price: Option<Decimal>,
144    pub trigger_quantity: Option<TriggerQuantity>,
145    pub triggered_at: Option<i64>,
146    pub time_in_force: TimeInForce,
147    pub related_order_id: Option<String>,
148    pub self_trade_prevention: SelfTradePrevention,
149    pub post_only: bool,
150    pub reduce_only: Option<bool>,
151    pub status: OrderStatus,
152    pub created_at: i64,
153}
154
155#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)]
156#[strum(serialize_all = "PascalCase")]
157#[serde(rename_all = "PascalCase")]
158pub enum OrderType {
159    #[default]
160    #[serde(rename(deserialize = "LIMIT"))]
161    Limit,
162    #[serde(rename(deserialize = "MARKET"))]
163    Market,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(tag = "orderType")]
168pub enum Order {
169    Market(MarketOrder),
170    Limit(LimitOrder),
171}
172
173#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)]
174#[strum(serialize_all = "UPPERCASE")]
175#[serde(rename_all = "UPPERCASE")]
176pub enum TimeInForce {
177    #[default]
178    GTC,
179    IOC,
180    FOK,
181}
182
183#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)]
184#[strum(serialize_all = "PascalCase")]
185#[serde(rename_all = "PascalCase")]
186pub enum SelfTradePrevention {
187    #[default]
188    RejectTaker,
189    RejectMaker,
190    RejectBoth,
191    Allow,
192}
193
194#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)]
195#[strum(serialize_all = "PascalCase")]
196#[serde(rename_all = "PascalCase")]
197pub enum OrderStatus {
198    Cancelled,
199    Expired,
200    Filled,
201    #[default]
202    New,
203    PartiallyFilled,
204    Triggered,
205    TriggerPending,
206}
207
208#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash)]
209#[strum(serialize_all = "PascalCase")]
210#[serde(rename_all = "PascalCase")]
211pub enum Side {
212    #[default]
213    Bid,
214    Ask,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, Default)]
218#[serde(rename_all = "camelCase")]
219pub struct ExecuteOrderPayload {
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub auto_lend: Option<bool>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub auto_lend_redeem: Option<bool>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub auto_borrow: Option<bool>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub auto_borrow_repay: Option<bool>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub client_id: Option<u32>,
230    pub order_type: OrderType,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub post_only: Option<bool>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub price: Option<Decimal>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub quantity: Option<Decimal>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub quote_quantity: Option<Decimal>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub reduce_only: Option<bool>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub self_trade_prevention: Option<SelfTradePrevention>,
243    pub side: Side,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub stop_loss_limit_price: Option<Decimal>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub stop_loss_trigger_by: Option<TriggerBy>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub stop_loss_trigger_price: Option<Decimal>,
250    pub symbol: String,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub take_profit_limit_price: Option<Decimal>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub take_profit_trigger_by: Option<TriggerBy>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub take_profit_trigger_price: Option<Decimal>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub time_in_force: Option<TimeInForce>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub trigger_by: Option<TriggerBy>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub trigger_price: Option<Decimal>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub trigger_quantity: Option<TriggerQuantity>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, Default)]
268#[serde(rename_all = "camelCase")]
269pub struct CancelOrderPayload {
270    pub symbol: String,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub order_id: Option<String>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub client_id: Option<u32>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, Default)]
278#[serde(rename_all = "camelCase")]
279pub struct CancelOpenOrdersPayload {
280    pub symbol: String,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub enum OrderUpdateType {
286    OrderAccepted,
287    OrderCancelled,
288    OrderExpired,
289    OrderFill,
290    OrderModified,
291    TriggerPlaced,
292    TriggerFailed,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct OrderUpdate {
298    /// Event type
299    #[serde(rename = "e")]
300    pub event_type: OrderUpdateType,
301
302    /// Event timestamp in microseconds
303    #[serde(rename = "E")]
304    pub event_time: i64,
305
306    /// Symbol
307    #[serde(rename = "s")]
308    pub symbol: String,
309
310    /// Client order id
311    #[serde(rename = "c")]
312    pub client_order_id: Option<u64>,
313
314    /// Side
315    #[serde(rename = "S")]
316    pub side: Side,
317
318    /// Order type
319    #[serde(rename = "o")]
320    pub order_type: OrderType,
321
322    /// Time in force
323    #[serde(rename = "f")]
324    pub time_in_force: TimeInForce,
325
326    /// Quantity
327    #[serde(rename = "q")]
328    pub quantity: Decimal,
329
330    /// Quantity in quote
331    #[serde(rename = "Q")]
332    pub quantity_in_quote: Option<Decimal>,
333
334    /// price
335    #[serde(rename = "p")]
336    pub price: Option<Decimal>,
337
338    /// trigger price
339    #[serde(rename = "P")]
340    pub trigger_price: Option<Decimal>,
341
342    /// trigger by
343    #[serde(rename = "B")]
344    pub trigger_by: Option<TriggerBy>,
345
346    /// Take profit trigger price
347    #[serde(rename = "a")]
348    pub take_profit_trigger_price: Option<Decimal>,
349
350    /// Stop loss trigger price
351    #[serde(rename = "b")]
352    pub stop_loss_trigger_price: Option<Decimal>,
353
354    /// Take profit trigger by
355    #[serde(rename = "d")]
356    pub take_profit_trigger_by: Option<TriggerBy>,
357
358    /// Stop loss trigger by
359    #[serde(rename = "g")]
360    pub stop_loss_trigger_by: Option<TriggerBy>,
361
362    /// Trigger quantity
363    #[serde(rename = "Y")]
364    pub trigger_quantity: Option<Decimal>,
365
366    /// Order State
367    #[serde(rename = "X")]
368    pub order_status: OrderStatus,
369
370    /// Order expiry reason
371    #[serde(rename = "R")]
372    pub order_expiry_reason: Option<String>,
373
374    /// Order ID
375    #[serde(rename = "i")]
376    pub order_id: String,
377
378    /// Trade ID
379    #[serde(rename = "t")]
380    pub trade_id: Option<u64>,
381
382    /// Fill quantity
383    #[serde(rename = "l")]
384    pub fill_quantity: Option<Decimal>,
385
386    /// Executed quantity
387    #[serde(rename = "z")]
388    pub executed_quantity: Decimal,
389
390    /// Executed quantity in quote
391    #[serde(rename = "Z")]
392    pub executed_quantity_in_quote: Decimal,
393
394    /// Fill price
395    #[serde(rename = "L")]
396    pub fill_price: Option<Decimal>,
397
398    /// Fill price
399    #[serde(rename = "m")]
400    pub was_maker: Option<bool>,
401
402    /// Fee
403    #[serde(rename = "n")]
404    pub fee: Option<Decimal>,
405
406    /// Fee symbol
407    #[serde(rename = "N")]
408    pub fee_symbol: Option<String>,
409
410    /// Self trade prevention
411    #[serde(rename = "V")]
412    pub self_trade_prevention: SelfTradePrevention,
413
414    /// Engine timestamp in microseconds
415    #[serde(rename = "T")]
416    pub timestamp: i64,
417
418    /// Origin of the update
419    #[serde(rename = "O")]
420    pub origin_of_the_update: String,
421
422    /// Related order ID
423    #[serde(rename = "I")]
424    pub related_order_id: Option<u64>,
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use rust_decimal_macros::dec;
431    use serde_json::json;
432
433    #[test]
434    fn both_forms_round_trip() {
435        let q: TriggerQuantity = serde_json::from_value(json!("12.5%")).unwrap();
436        assert_eq!(q, TriggerQuantity::Percent(dec!(12.5)));
437
438        let q: TriggerQuantity = serde_json::from_value(json!("0.01")).unwrap();
439        assert_eq!(q, TriggerQuantity::Amount(dec!(0.01)));
440    }
441
442    #[test]
443    fn test_trigger_quantity_serialize() {
444        let trigger_quantity = TriggerQuantity::Percent(dec!(100));
445        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
446        assert_eq!(trigger_quantity_str, "\"100%\"");
447
448        let trigger_quantity = TriggerQuantity::Percent(dec!(75.50));
449        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
450        assert_eq!(trigger_quantity_str, "\"75.50%\"");
451
452        let trigger_quantity = TriggerQuantity::Amount(dec!(100));
453        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
454        assert_eq!(trigger_quantity_str, "\"100\"");
455
456        let trigger_quantity = TriggerQuantity::Amount(dec!(75.50));
457        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
458        assert_eq!(trigger_quantity_str, "\"75.50\"");
459    }
460
461    #[test]
462    fn test_trigger_by_serialize() {
463        let trigger_by_last = TriggerBy::LastPrice;
464        let trigger_by_last_str = serde_json::to_string(&trigger_by_last).unwrap();
465        assert_eq!(trigger_by_last_str, "\"LastPrice\"");
466
467        let trigger_by_mark = TriggerBy::MarkPrice;
468        let trigger_by_mark_str = serde_json::to_string(&trigger_by_mark).unwrap();
469        assert_eq!(trigger_by_mark_str, "\"MarkPrice\"");
470
471        let trigger_by_index = TriggerBy::IndexPrice;
472        let trigger_by_index_str = serde_json::to_string(&trigger_by_index).unwrap();
473        assert_eq!(trigger_by_index_str, "\"IndexPrice\"");
474    }
475
476    #[test]
477    fn test_order_update() {
478        let data = r#"
479        {"E":1748288167010366,"O":"USER","P":"178.05","Q":"0","S":"Ask","T":1748288167009460,"V":"RejectTaker","X":"TriggerPending","Y":"20.03","Z":"0","e":"triggerPlaced","f":"GTC","i":"114575813313101824","o":"LIMIT","p":"178.15","q":"0","r":false,"s":"SOL_USDC","t":null,"z":"0"}
480        "#;
481
482        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
483        assert_eq!(order_update.price.unwrap(), dec!(178.15));
484        assert_eq!(order_update.trigger_price.unwrap(), dec!(178.05));
485        assert_eq!(order_update.trigger_quantity.unwrap(), dec!(20.03));
486        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(0));
487
488        let data = r#"
489        {"E":1748288615134547,"O":"USER","Q":"3568.3445","S":"Ask","T":1748288615133255,"V":"RejectTaker","X":"New","Z":"0","e":"orderAccepted","f":"GTC","i":"114575842681290753","o":"LIMIT","p":"178.15","q":"20.03","r":false,"s":"SOL_USDC","t":null,"z":"0"}
490        "#;
491
492        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
493        assert_eq!(order_update.price.unwrap(), dec!(178.15));
494        assert_eq!(order_update.trigger_price, None);
495        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(3568.3445));
496        assert_eq!(order_update.quantity, dec!(20.03));
497
498        let data = r#"
499        {"B":"LastPrice","E":1748289564405220,"O":"USER","P":"178.55","S":"Ask","T":1748289564404373,"V":"RejectTaker","X":"Cancelled","Y":"1","Z":"0","e":"orderCancelled","f":"GTC","i":"114575904705282048","o":"MARKET","q":"0","r":false,"s":"SOL_USDC","t":null,"z":"0"}
500        "#;
501        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
502        assert_eq!(order_update.trigger_price.unwrap(), dec!(178.55));
503    }
504}