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 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 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 #[serde(skip_serializing_if = "Option::is_none")]
300 pub slippage_tolerance: Option<Decimal>,
301 #[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 #[serde(rename = "e")]
339 pub event_type: OrderUpdateType,
340
341 #[serde(rename = "E")]
343 pub event_time: i64,
344
345 #[serde(rename = "s")]
347 pub symbol: String,
348
349 #[serde(rename = "c")]
351 pub client_order_id: Option<u64>,
352
353 #[serde(rename = "S")]
355 pub side: Side,
356
357 #[serde(rename = "o")]
359 pub order_type: OrderType,
360
361 #[serde(rename = "f")]
363 pub time_in_force: TimeInForce,
364
365 #[serde(rename = "q")]
367 pub quantity: Decimal,
368
369 #[serde(rename = "Q")]
371 pub quantity_in_quote: Option<Decimal>,
372
373 #[serde(rename = "p")]
375 pub price: Option<Decimal>,
376
377 #[serde(rename = "P")]
379 pub trigger_price: Option<Decimal>,
380
381 #[serde(rename = "B")]
383 pub trigger_by: Option<TriggerBy>,
384
385 #[serde(rename = "a")]
387 pub take_profit_trigger_price: Option<Decimal>,
388
389 #[serde(rename = "b")]
391 pub stop_loss_trigger_price: Option<Decimal>,
392
393 #[serde(rename = "d")]
395 pub take_profit_trigger_by: Option<TriggerBy>,
396
397 #[serde(rename = "g")]
399 pub stop_loss_trigger_by: Option<TriggerBy>,
400
401 #[serde(rename = "Y")]
403 pub trigger_quantity: Option<TriggerQuantity>,
404
405 #[serde(rename = "X")]
407 pub order_status: OrderStatus,
408
409 #[serde(rename = "R")]
411 pub order_expiry_reason: Option<String>,
412
413 #[serde(rename = "i")]
415 pub order_id: String,
416
417 #[serde(rename = "t")]
419 pub trade_id: Option<u64>,
420
421 #[serde(rename = "l")]
423 pub fill_quantity: Option<Decimal>,
424
425 #[serde(rename = "z")]
427 pub executed_quantity: Decimal,
428
429 #[serde(rename = "Z")]
431 pub executed_quantity_in_quote: Decimal,
432
433 #[serde(rename = "L")]
435 pub fill_price: Option<Decimal>,
436
437 #[serde(rename = "m")]
439 pub was_maker: Option<bool>,
440
441 #[serde(rename = "n")]
443 pub fee: Option<Decimal>,
444
445 #[serde(rename = "N")]
447 pub fee_symbol: Option<String>,
448
449 #[serde(rename = "V")]
451 pub self_trade_prevention: SelfTradePrevention,
452
453 #[serde(rename = "T")]
455 pub timestamp: i64,
456
457 #[serde(rename = "O")]
459 pub origin_of_the_update: String,
460
461 #[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#[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}