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