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 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 #[serde(skip_serializing_if = "Option::is_none")]
285 pub slippage_tolerance: Option<Decimal>,
286 #[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 #[serde(rename = "e")]
324 pub event_type: OrderUpdateType,
325
326 #[serde(rename = "E")]
328 pub event_time: i64,
329
330 #[serde(rename = "s")]
332 pub symbol: String,
333
334 #[serde(rename = "c")]
336 pub client_order_id: Option<u64>,
337
338 #[serde(rename = "S")]
340 pub side: Side,
341
342 #[serde(rename = "o")]
344 pub order_type: OrderType,
345
346 #[serde(rename = "f")]
348 pub time_in_force: TimeInForce,
349
350 #[serde(rename = "q")]
352 pub quantity: Decimal,
353
354 #[serde(rename = "Q")]
356 pub quantity_in_quote: Option<Decimal>,
357
358 #[serde(rename = "p")]
360 pub price: Option<Decimal>,
361
362 #[serde(rename = "P")]
364 pub trigger_price: Option<Decimal>,
365
366 #[serde(rename = "B")]
368 pub trigger_by: Option<TriggerBy>,
369
370 #[serde(rename = "a")]
372 pub take_profit_trigger_price: Option<Decimal>,
373
374 #[serde(rename = "b")]
376 pub stop_loss_trigger_price: Option<Decimal>,
377
378 #[serde(rename = "d")]
380 pub take_profit_trigger_by: Option<TriggerBy>,
381
382 #[serde(rename = "g")]
384 pub stop_loss_trigger_by: Option<TriggerBy>,
385
386 #[serde(rename = "Y")]
388 pub trigger_quantity: Option<TriggerQuantity>,
389
390 #[serde(rename = "X")]
392 pub order_status: OrderStatus,
393
394 #[serde(rename = "R")]
396 pub order_expiry_reason: Option<String>,
397
398 #[serde(rename = "i")]
400 pub order_id: String,
401
402 #[serde(rename = "t")]
404 pub trade_id: Option<u64>,
405
406 #[serde(rename = "l")]
408 pub fill_quantity: Option<Decimal>,
409
410 #[serde(rename = "z")]
412 pub executed_quantity: Decimal,
413
414 #[serde(rename = "Z")]
416 pub executed_quantity_in_quote: Decimal,
417
418 #[serde(rename = "L")]
420 pub fill_price: Option<Decimal>,
421
422 #[serde(rename = "m")]
424 pub was_maker: Option<bool>,
425
426 #[serde(rename = "n")]
428 pub fee: Option<Decimal>,
429
430 #[serde(rename = "N")]
432 pub fee_symbol: Option<String>,
433
434 #[serde(rename = "V")]
436 pub self_trade_prevention: SelfTradePrevention,
437
438 #[serde(rename = "T")]
440 pub timestamp: i64,
441
442 #[serde(rename = "O")]
444 pub origin_of_the_update: String,
445
446 #[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#[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}