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