bpx_api_types/
order.rs

1use std::{fmt, str::FromStr};
2
3use rust_decimal::{Decimal, prelude::FromPrimitive};
4use serde::{Deserialize, Deserializer, Serialize, de::Visitor};
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(
156    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
157)]
158#[strum(serialize_all = "PascalCase")]
159#[serde(rename_all = "PascalCase")]
160pub enum OrderType {
161    #[default]
162    #[serde(rename(deserialize = "LIMIT"))]
163    Limit,
164    #[serde(rename(deserialize = "MARKET"))]
165    Market,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(tag = "orderType")]
170pub enum Order {
171    Market(MarketOrder),
172    Limit(LimitOrder),
173}
174
175#[derive(
176    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
177)]
178#[strum(serialize_all = "UPPERCASE")]
179#[serde(rename_all = "UPPERCASE")]
180pub enum TimeInForce {
181    #[default]
182    GTC,
183    IOC,
184    FOK,
185}
186
187#[derive(
188    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
189)]
190#[strum(serialize_all = "PascalCase")]
191#[serde(rename_all = "PascalCase")]
192pub enum SelfTradePrevention {
193    #[default]
194    RejectTaker,
195    RejectMaker,
196    RejectBoth,
197    Allow,
198}
199
200#[derive(
201    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
202)]
203#[strum(serialize_all = "PascalCase")]
204#[serde(rename_all = "PascalCase")]
205pub enum OrderStatus {
206    Cancelled,
207    Expired,
208    Filled,
209    #[default]
210    New,
211    PartiallyFilled,
212    Triggered,
213    TriggerPending,
214}
215
216#[derive(
217    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
218)]
219#[strum(serialize_all = "PascalCase")]
220#[serde(rename_all = "PascalCase")]
221pub enum Side {
222    #[default]
223    Bid,
224    Ask,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, Default)]
228#[serde(rename_all = "camelCase")]
229pub struct ExecuteOrderPayload {
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub auto_lend: Option<bool>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub auto_lend_redeem: Option<bool>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub auto_borrow: Option<bool>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub auto_borrow_repay: Option<bool>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub client_id: Option<u32>,
240    pub order_type: OrderType,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub post_only: Option<bool>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub price: Option<Decimal>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub quantity: Option<Decimal>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub quote_quantity: Option<Decimal>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub reduce_only: Option<bool>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub self_trade_prevention: Option<SelfTradePrevention>,
253    pub side: Side,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub stop_loss_limit_price: Option<Decimal>,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub stop_loss_trigger_by: Option<TriggerBy>,
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub stop_loss_trigger_price: Option<Decimal>,
260    pub symbol: String,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub take_profit_limit_price: Option<Decimal>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub take_profit_trigger_by: Option<TriggerBy>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub take_profit_trigger_price: Option<Decimal>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub time_in_force: Option<TimeInForce>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub trigger_by: Option<TriggerBy>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub trigger_price: Option<Decimal>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub trigger_quantity: Option<TriggerQuantity>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, Default)]
278#[serde(rename_all = "camelCase")]
279pub struct CancelOrderPayload {
280    pub symbol: String,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub order_id: Option<String>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub client_id: Option<u32>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288#[serde(rename_all = "camelCase")]
289pub struct CancelOpenOrdersPayload {
290    pub symbol: String,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub enum OrderUpdateType {
296    OrderAccepted,
297    OrderCancelled,
298    OrderExpired,
299    OrderFill,
300    OrderModified,
301    TriggerPlaced,
302    TriggerFailed,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct OrderUpdate {
308    /// Event type
309    #[serde(rename = "e")]
310    pub event_type: OrderUpdateType,
311
312    /// Event timestamp in microseconds
313    #[serde(rename = "E")]
314    pub event_time: i64,
315
316    /// Symbol
317    #[serde(rename = "s")]
318    pub symbol: String,
319
320    /// Client order id
321    #[serde(rename = "c")]
322    pub client_order_id: Option<u64>,
323
324    /// Side
325    #[serde(rename = "S")]
326    pub side: Side,
327
328    /// Order type
329    #[serde(rename = "o")]
330    pub order_type: OrderType,
331
332    /// Time in force
333    #[serde(rename = "f")]
334    pub time_in_force: TimeInForce,
335
336    /// Quantity
337    #[serde(rename = "q")]
338    pub quantity: Decimal,
339
340    /// Quantity in quote
341    #[serde(rename = "Q")]
342    pub quantity_in_quote: Option<Decimal>,
343
344    /// price
345    #[serde(rename = "p")]
346    pub price: Option<Decimal>,
347
348    /// trigger price
349    #[serde(rename = "P")]
350    pub trigger_price: Option<Decimal>,
351
352    /// trigger by
353    #[serde(rename = "B")]
354    pub trigger_by: Option<TriggerBy>,
355
356    /// Take profit trigger price
357    #[serde(rename = "a")]
358    pub take_profit_trigger_price: Option<Decimal>,
359
360    /// Stop loss trigger price
361    #[serde(rename = "b")]
362    pub stop_loss_trigger_price: Option<Decimal>,
363
364    /// Take profit trigger by
365    #[serde(rename = "d")]
366    pub take_profit_trigger_by: Option<TriggerBy>,
367
368    /// Stop loss trigger by
369    #[serde(rename = "g")]
370    pub stop_loss_trigger_by: Option<TriggerBy>,
371
372    /// Trigger quantity
373    #[serde(rename = "Y")]
374    pub trigger_quantity: Option<Decimal>,
375
376    /// Order State
377    #[serde(rename = "X")]
378    pub order_status: OrderStatus,
379
380    /// Order expiry reason
381    #[serde(rename = "R")]
382    pub order_expiry_reason: Option<String>,
383
384    /// Order ID
385    #[serde(rename = "i")]
386    pub order_id: String,
387
388    /// Trade ID
389    #[serde(rename = "t")]
390    pub trade_id: Option<u64>,
391
392    /// Fill quantity
393    #[serde(rename = "l")]
394    pub fill_quantity: Option<Decimal>,
395
396    /// Executed quantity
397    #[serde(rename = "z")]
398    pub executed_quantity: Decimal,
399
400    /// Executed quantity in quote
401    #[serde(rename = "Z")]
402    pub executed_quantity_in_quote: Decimal,
403
404    /// Fill price
405    #[serde(rename = "L")]
406    pub fill_price: Option<Decimal>,
407
408    /// Fill price
409    #[serde(rename = "m")]
410    pub was_maker: Option<bool>,
411
412    /// Fee
413    #[serde(rename = "n")]
414    pub fee: Option<Decimal>,
415
416    /// Fee symbol
417    #[serde(rename = "N")]
418    pub fee_symbol: Option<String>,
419
420    /// Self trade prevention
421    #[serde(rename = "V")]
422    pub self_trade_prevention: SelfTradePrevention,
423
424    /// Engine timestamp in microseconds
425    #[serde(rename = "T")]
426    pub timestamp: i64,
427
428    /// Origin of the update
429    #[serde(rename = "O")]
430    pub origin_of_the_update: String,
431
432    /// Related order ID
433    #[serde(rename = "I")]
434    pub related_order_id: Option<u64>,
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use rust_decimal_macros::dec;
441    use serde_json::json;
442
443    #[test]
444    fn both_forms_round_trip() {
445        let q: TriggerQuantity = serde_json::from_value(json!("12.5%")).unwrap();
446        assert_eq!(q, TriggerQuantity::Percent(dec!(12.5)));
447
448        let q: TriggerQuantity = serde_json::from_value(json!("0.01")).unwrap();
449        assert_eq!(q, TriggerQuantity::Amount(dec!(0.01)));
450    }
451
452    #[test]
453    fn test_trigger_quantity_serialize() {
454        let trigger_quantity = TriggerQuantity::Percent(dec!(100));
455        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
456        assert_eq!(trigger_quantity_str, "\"100%\"");
457
458        let trigger_quantity = TriggerQuantity::Percent(dec!(75.50));
459        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
460        assert_eq!(trigger_quantity_str, "\"75.50%\"");
461
462        let trigger_quantity = TriggerQuantity::Amount(dec!(100));
463        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
464        assert_eq!(trigger_quantity_str, "\"100\"");
465
466        let trigger_quantity = TriggerQuantity::Amount(dec!(75.50));
467        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
468        assert_eq!(trigger_quantity_str, "\"75.50\"");
469    }
470
471    #[test]
472    fn test_trigger_by_serialize() {
473        let trigger_by_last = TriggerBy::LastPrice;
474        let trigger_by_last_str = serde_json::to_string(&trigger_by_last).unwrap();
475        assert_eq!(trigger_by_last_str, "\"LastPrice\"");
476
477        let trigger_by_mark = TriggerBy::MarkPrice;
478        let trigger_by_mark_str = serde_json::to_string(&trigger_by_mark).unwrap();
479        assert_eq!(trigger_by_mark_str, "\"MarkPrice\"");
480
481        let trigger_by_index = TriggerBy::IndexPrice;
482        let trigger_by_index_str = serde_json::to_string(&trigger_by_index).unwrap();
483        assert_eq!(trigger_by_index_str, "\"IndexPrice\"");
484    }
485
486    #[test]
487    fn test_order_update() {
488        let data = r#"
489        {"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"}
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.unwrap(), dec!(178.05));
495        assert_eq!(order_update.trigger_quantity.unwrap(), dec!(20.03));
496        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(0));
497
498        let data = r#"
499        {"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"}
500        "#;
501
502        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
503        assert_eq!(order_update.price.unwrap(), dec!(178.15));
504        assert_eq!(order_update.trigger_price, None);
505        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(3568.3445));
506        assert_eq!(order_update.quantity, dec!(20.03));
507
508        let data = r#"
509        {"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"}
510        "#;
511        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
512        assert_eq!(order_update.trigger_price.unwrap(), dec!(178.55));
513    }
514}