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 SystemOrderType {
222    #[default]
223    LiquidatePositionOnBook,
224    LiquidatePositionOnBackstop,
225    LiquidatePositionOnAdl,
226    CollateralConversion,
227    FutureExpiry,
228    OrderBookClosed,
229}
230
231#[derive(
232    Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
233)]
234#[strum(serialize_all = "PascalCase")]
235#[serde(rename_all = "PascalCase")]
236pub enum Side {
237    #[default]
238    Bid,
239    Ask,
240}
241
242#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, EnumString, PartialEq, Eq, Hash)]
243#[strum(serialize_all = "PascalCase")]
244#[serde(rename_all = "PascalCase")]
245pub enum SlippageToleranceType {
246    TickSize,
247    Percent,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, Default)]
251#[serde(rename_all = "camelCase")]
252pub struct ExecuteOrderPayload {
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub auto_lend: Option<bool>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub auto_lend_redeem: Option<bool>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub auto_borrow: Option<bool>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub auto_borrow_repay: Option<bool>,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub client_id: Option<u32>,
263    pub order_type: OrderType,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub post_only: Option<bool>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub price: Option<Decimal>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub quantity: Option<Decimal>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub quote_quantity: Option<Decimal>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub reduce_only: Option<bool>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub self_trade_prevention: Option<SelfTradePrevention>,
276    pub side: Side,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub stop_loss_limit_price: Option<Decimal>,
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub stop_loss_trigger_by: Option<TriggerBy>,
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub stop_loss_trigger_price: Option<Decimal>,
283    pub symbol: String,
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub take_profit_limit_price: Option<Decimal>,
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub take_profit_trigger_by: Option<TriggerBy>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub take_profit_trigger_price: Option<Decimal>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub time_in_force: Option<TimeInForce>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub trigger_by: Option<TriggerBy>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub trigger_price: Option<Decimal>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub trigger_quantity: Option<TriggerQuantity>,
298    /// Slippage tolerance allowed for the order.
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub slippage_tolerance: Option<Decimal>,
301    /// Slippage tolerance type.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub slippage_tolerance_type: Option<SlippageToleranceType>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, Default)]
307#[serde(rename_all = "camelCase")]
308pub struct CancelOrderPayload {
309    pub symbol: String,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub order_id: Option<String>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub client_id: Option<u32>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, Default)]
317#[serde(rename_all = "camelCase")]
318pub struct CancelOpenOrdersPayload {
319    pub symbol: String,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub enum OrderUpdateType {
325    OrderAccepted,
326    OrderCancelled,
327    OrderExpired,
328    OrderFill,
329    OrderModified,
330    TriggerPlaced,
331    TriggerFailed,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct OrderUpdate {
337    /// Event type
338    #[serde(rename = "e")]
339    pub event_type: OrderUpdateType,
340
341    /// Event timestamp in microseconds
342    #[serde(rename = "E")]
343    pub event_time: i64,
344
345    /// Symbol
346    #[serde(rename = "s")]
347    pub symbol: String,
348
349    /// Client order id
350    #[serde(rename = "c")]
351    pub client_order_id: Option<u64>,
352
353    /// Side
354    #[serde(rename = "S")]
355    pub side: Side,
356
357    /// Order type
358    #[serde(rename = "o")]
359    pub order_type: OrderType,
360
361    /// Time in force
362    #[serde(rename = "f")]
363    pub time_in_force: TimeInForce,
364
365    /// Quantity
366    #[serde(rename = "q")]
367    pub quantity: Decimal,
368
369    /// Quantity in quote
370    #[serde(rename = "Q")]
371    pub quantity_in_quote: Option<Decimal>,
372
373    /// price
374    #[serde(rename = "p")]
375    pub price: Option<Decimal>,
376
377    /// trigger price
378    #[serde(rename = "P")]
379    pub trigger_price: Option<Decimal>,
380
381    /// trigger by
382    #[serde(rename = "B")]
383    pub trigger_by: Option<TriggerBy>,
384
385    /// Take profit trigger price
386    #[serde(rename = "a")]
387    pub take_profit_trigger_price: Option<Decimal>,
388
389    /// Stop loss trigger price
390    #[serde(rename = "b")]
391    pub stop_loss_trigger_price: Option<Decimal>,
392
393    /// Take profit trigger by
394    #[serde(rename = "d")]
395    pub take_profit_trigger_by: Option<TriggerBy>,
396
397    /// Stop loss trigger by
398    #[serde(rename = "g")]
399    pub stop_loss_trigger_by: Option<TriggerBy>,
400
401    /// Trigger quantity
402    #[serde(rename = "Y")]
403    pub trigger_quantity: Option<TriggerQuantity>,
404
405    /// Order State
406    #[serde(rename = "X")]
407    pub order_status: OrderStatus,
408
409    /// Order expiry reason
410    #[serde(rename = "R")]
411    pub order_expiry_reason: Option<String>,
412
413    /// Order ID
414    #[serde(rename = "i")]
415    pub order_id: String,
416
417    /// Trade ID
418    #[serde(rename = "t")]
419    pub trade_id: Option<u64>,
420
421    /// Fill quantity
422    #[serde(rename = "l")]
423    pub fill_quantity: Option<Decimal>,
424
425    /// Executed quantity
426    #[serde(rename = "z")]
427    pub executed_quantity: Decimal,
428
429    /// Executed quantity in quote
430    #[serde(rename = "Z")]
431    pub executed_quantity_in_quote: Decimal,
432
433    /// Fill price
434    #[serde(rename = "L")]
435    pub fill_price: Option<Decimal>,
436
437    /// Fill price
438    #[serde(rename = "m")]
439    pub was_maker: Option<bool>,
440
441    /// Fee
442    #[serde(rename = "n")]
443    pub fee: Option<Decimal>,
444
445    /// Fee symbol
446    #[serde(rename = "N")]
447    pub fee_symbol: Option<String>,
448
449    /// Self trade prevention
450    #[serde(rename = "V")]
451    pub self_trade_prevention: SelfTradePrevention,
452
453    /// Engine timestamp in microseconds
454    #[serde(rename = "T")]
455    pub timestamp: i64,
456
457    /// Origin of the update
458    #[serde(rename = "O")]
459    pub origin_of_the_update: String,
460
461    /// Related order ID
462    #[serde(rename = "I")]
463    pub related_order_id: Option<u64>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct OrderError {
468    pub code: String,
469    pub message: String,
470    pub operation: String,
471}
472
473/// An item in the response for a batch order execution
474/// which can be either a successful order or an error.
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[serde(untagged)]
477#[allow(clippy::large_enum_variant)]
478pub enum BatchOrderResponse {
479    Order(Order),
480    Error(OrderError),
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use rust_decimal_macros::dec;
487    use serde_json::json;
488
489    #[test]
490    fn both_forms_round_trip() {
491        let q: TriggerQuantity = serde_json::from_value(json!("12.5%")).unwrap();
492        assert_eq!(q, TriggerQuantity::Percent(dec!(12.5)));
493
494        let q: TriggerQuantity = serde_json::from_value(json!("0.01")).unwrap();
495        assert_eq!(q, TriggerQuantity::Amount(dec!(0.01)));
496    }
497
498    #[test]
499    fn test_trigger_quantity_serialize() {
500        let trigger_quantity = TriggerQuantity::Percent(dec!(100));
501        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
502        assert_eq!(trigger_quantity_str, "\"100%\"");
503
504        let trigger_quantity = TriggerQuantity::Percent(dec!(75.50));
505        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
506        assert_eq!(trigger_quantity_str, "\"75.50%\"");
507
508        let trigger_quantity = TriggerQuantity::Amount(dec!(100));
509        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
510        assert_eq!(trigger_quantity_str, "\"100\"");
511
512        let trigger_quantity = TriggerQuantity::Amount(dec!(75.50));
513        let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
514        assert_eq!(trigger_quantity_str, "\"75.50\"");
515    }
516
517    #[test]
518    fn test_trigger_by_serialize() {
519        let trigger_by_last = TriggerBy::LastPrice;
520        let trigger_by_last_str = serde_json::to_string(&trigger_by_last).unwrap();
521        assert_eq!(trigger_by_last_str, "\"LastPrice\"");
522
523        let trigger_by_mark = TriggerBy::MarkPrice;
524        let trigger_by_mark_str = serde_json::to_string(&trigger_by_mark).unwrap();
525        assert_eq!(trigger_by_mark_str, "\"MarkPrice\"");
526
527        let trigger_by_index = TriggerBy::IndexPrice;
528        let trigger_by_index_str = serde_json::to_string(&trigger_by_index).unwrap();
529        assert_eq!(trigger_by_index_str, "\"IndexPrice\"");
530    }
531
532    #[test]
533    fn test_order_update() {
534        let data = r#"
535        {"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"}
536        "#;
537
538        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
539        assert_eq!(order_update.price.unwrap(), dec!(178.15));
540        assert_eq!(order_update.trigger_price.unwrap(), dec!(178.05));
541        assert_eq!(
542            order_update.trigger_quantity.unwrap(),
543            TriggerQuantity::Amount(dec!(20.03))
544        );
545        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(0));
546
547        let data = r#"
548        {"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"}
549        "#;
550
551        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
552        assert_eq!(order_update.price.unwrap(), dec!(178.15));
553        assert_eq!(order_update.trigger_price, None);
554        assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(3568.3445));
555        assert_eq!(order_update.quantity, dec!(20.03));
556
557        let data = r#"
558        {"B":"LastPrice","E":1748289564405220,"O":"USER","P":"178.55","S":"Ask","T":1748289564404373,"V":"RejectTaker","X":"Cancelled","Y":"80%","Z":"0","e":"orderCancelled","f":"GTC","i":"114575904705282048","o":"MARKET","q":"0","r":false,"s":"SOL_USDC","t":null,"z":"0"}
559        "#;
560        let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
561        assert_eq!(order_update.trigger_price.unwrap(), dec!(178.55));
562        assert_eq!(
563            order_update.trigger_quantity.unwrap(),
564            TriggerQuantity::Percent(dec!(80))
565        );
566    }
567}