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, Clone, Serialize, Deserialize, Default)]
228#[serde(rename_all = "camelCase")]
229pub struct ExecuteOrderPayload {
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub auto_lend: Option<bool>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub auto_lend_redeem: Option<bool>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub auto_borrow: Option<bool>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub auto_borrow_repay: Option<bool>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub client_id: Option<u32>,
240 pub order_type: OrderType,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub post_only: Option<bool>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub price: Option<Decimal>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub quantity: Option<Decimal>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub quote_quantity: Option<Decimal>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub reduce_only: Option<bool>,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub self_trade_prevention: Option<SelfTradePrevention>,
253 pub side: Side,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub stop_loss_limit_price: Option<Decimal>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub stop_loss_trigger_by: Option<TriggerBy>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub stop_loss_trigger_price: Option<Decimal>,
260 pub symbol: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub take_profit_limit_price: Option<Decimal>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub take_profit_trigger_by: Option<TriggerBy>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub take_profit_trigger_price: Option<Decimal>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub time_in_force: Option<TimeInForce>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub trigger_by: Option<TriggerBy>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub trigger_price: Option<Decimal>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub trigger_quantity: Option<TriggerQuantity>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, Default)]
278#[serde(rename_all = "camelCase")]
279pub struct CancelOrderPayload {
280 pub symbol: String,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub order_id: Option<String>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub client_id: Option<u32>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288#[serde(rename_all = "camelCase")]
289pub struct CancelOpenOrdersPayload {
290 pub symbol: String,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub enum OrderUpdateType {
296 OrderAccepted,
297 OrderCancelled,
298 OrderExpired,
299 OrderFill,
300 OrderModified,
301 TriggerPlaced,
302 TriggerFailed,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase")]
307pub struct OrderUpdate {
308 #[serde(rename = "e")]
310 pub event_type: OrderUpdateType,
311
312 #[serde(rename = "E")]
314 pub event_time: i64,
315
316 #[serde(rename = "s")]
318 pub symbol: String,
319
320 #[serde(rename = "c")]
322 pub client_order_id: Option<u64>,
323
324 #[serde(rename = "S")]
326 pub side: Side,
327
328 #[serde(rename = "o")]
330 pub order_type: OrderType,
331
332 #[serde(rename = "f")]
334 pub time_in_force: TimeInForce,
335
336 #[serde(rename = "q")]
338 pub quantity: Decimal,
339
340 #[serde(rename = "Q")]
342 pub quantity_in_quote: Option<Decimal>,
343
344 #[serde(rename = "p")]
346 pub price: Option<Decimal>,
347
348 #[serde(rename = "P")]
350 pub trigger_price: Option<Decimal>,
351
352 #[serde(rename = "B")]
354 pub trigger_by: Option<TriggerBy>,
355
356 #[serde(rename = "a")]
358 pub take_profit_trigger_price: Option<Decimal>,
359
360 #[serde(rename = "b")]
362 pub stop_loss_trigger_price: Option<Decimal>,
363
364 #[serde(rename = "d")]
366 pub take_profit_trigger_by: Option<TriggerBy>,
367
368 #[serde(rename = "g")]
370 pub stop_loss_trigger_by: Option<TriggerBy>,
371
372 #[serde(rename = "Y")]
374 pub trigger_quantity: Option<Decimal>,
375
376 #[serde(rename = "X")]
378 pub order_status: OrderStatus,
379
380 #[serde(rename = "R")]
382 pub order_expiry_reason: Option<String>,
383
384 #[serde(rename = "i")]
386 pub order_id: String,
387
388 #[serde(rename = "t")]
390 pub trade_id: Option<u64>,
391
392 #[serde(rename = "l")]
394 pub fill_quantity: Option<Decimal>,
395
396 #[serde(rename = "z")]
398 pub executed_quantity: Decimal,
399
400 #[serde(rename = "Z")]
402 pub executed_quantity_in_quote: Decimal,
403
404 #[serde(rename = "L")]
406 pub fill_price: Option<Decimal>,
407
408 #[serde(rename = "m")]
410 pub was_maker: Option<bool>,
411
412 #[serde(rename = "n")]
414 pub fee: Option<Decimal>,
415
416 #[serde(rename = "N")]
418 pub fee_symbol: Option<String>,
419
420 #[serde(rename = "V")]
422 pub self_trade_prevention: SelfTradePrevention,
423
424 #[serde(rename = "T")]
426 pub timestamp: i64,
427
428 #[serde(rename = "O")]
430 pub origin_of_the_update: String,
431
432 #[serde(rename = "I")]
434 pub related_order_id: Option<u64>,
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use rust_decimal_macros::dec;
441 use serde_json::json;
442
443 #[test]
444 fn both_forms_round_trip() {
445 let q: TriggerQuantity = serde_json::from_value(json!("12.5%")).unwrap();
446 assert_eq!(q, TriggerQuantity::Percent(dec!(12.5)));
447
448 let q: TriggerQuantity = serde_json::from_value(json!("0.01")).unwrap();
449 assert_eq!(q, TriggerQuantity::Amount(dec!(0.01)));
450 }
451
452 #[test]
453 fn test_trigger_quantity_serialize() {
454 let trigger_quantity = TriggerQuantity::Percent(dec!(100));
455 let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
456 assert_eq!(trigger_quantity_str, "\"100%\"");
457
458 let trigger_quantity = TriggerQuantity::Percent(dec!(75.50));
459 let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
460 assert_eq!(trigger_quantity_str, "\"75.50%\"");
461
462 let trigger_quantity = TriggerQuantity::Amount(dec!(100));
463 let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
464 assert_eq!(trigger_quantity_str, "\"100\"");
465
466 let trigger_quantity = TriggerQuantity::Amount(dec!(75.50));
467 let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
468 assert_eq!(trigger_quantity_str, "\"75.50\"");
469 }
470
471 #[test]
472 fn test_trigger_by_serialize() {
473 let trigger_by_last = TriggerBy::LastPrice;
474 let trigger_by_last_str = serde_json::to_string(&trigger_by_last).unwrap();
475 assert_eq!(trigger_by_last_str, "\"LastPrice\"");
476
477 let trigger_by_mark = TriggerBy::MarkPrice;
478 let trigger_by_mark_str = serde_json::to_string(&trigger_by_mark).unwrap();
479 assert_eq!(trigger_by_mark_str, "\"MarkPrice\"");
480
481 let trigger_by_index = TriggerBy::IndexPrice;
482 let trigger_by_index_str = serde_json::to_string(&trigger_by_index).unwrap();
483 assert_eq!(trigger_by_index_str, "\"IndexPrice\"");
484 }
485
486 #[test]
487 fn test_order_update() {
488 let data = r#"
489 {"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"}
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.unwrap(), dec!(178.05));
495 assert_eq!(order_update.trigger_quantity.unwrap(), dec!(20.03));
496 assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(0));
497
498 let data = r#"
499 {"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"}
500 "#;
501
502 let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
503 assert_eq!(order_update.price.unwrap(), dec!(178.15));
504 assert_eq!(order_update.trigger_price, None);
505 assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(3568.3445));
506 assert_eq!(order_update.quantity, dec!(20.03));
507
508 let data = r#"
509 {"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"}
510 "#;
511 let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
512 assert_eq!(order_update.trigger_price.unwrap(), dec!(178.55));
513 }
514}