1use rust_decimal::Decimal;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10
11pub fn dec<'de, D: Deserializer<'de>>(d: D) -> Result<Decimal, D::Error> {
13 Ok(opt_dec(d)?.unwrap_or(Decimal::ZERO))
14}
15
16pub fn opt_dec<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Decimal>, D::Error> {
18 use serde::de::Error;
19 let text = match Value::deserialize(d)? {
20 Value::Null => return Ok(None),
21 Value::String(s) => s,
22 Value::Number(n) => n.to_string(),
23 other => {
24 return Err(D::Error::custom(format!(
25 "a number was expected, got {other}"
26 )));
27 }
28 };
29 let text = text.trim();
30 if text.is_empty() {
31 return Ok(None);
32 }
33 text.parse::<Decimal>()
34 .or_else(|_| Decimal::from_scientific(text))
35 .map(Some)
36 .map_err(|e| D::Error::custom(format!("{text:?} is not a number: {e}")))
37}
38
39pub fn int<'de, D: Deserializer<'de>>(d: D) -> Result<i64, D::Error> {
41 use serde::de::Error;
42 match Value::deserialize(d)? {
43 Value::Null => Ok(0),
44 Value::Number(n) => n
45 .as_i64()
46 .or_else(|| n.as_f64().map(|f| f as i64))
47 .ok_or_else(|| D::Error::custom("an integer was expected")),
48 Value::String(s) if s.trim().is_empty() => Ok(0),
49 Value::String(s) => s
50 .trim()
51 .parse::<i64>()
52 .map_err(|e| D::Error::custom(format!("{s:?} is not an integer: {e}"))),
53 other => Err(D::Error::custom(format!(
54 "an integer was expected, got {other}"
55 ))),
56 }
57}
58
59pub fn ser_dec<S: Serializer>(value: &Decimal, s: S) -> Result<S::Ok, S::Error> {
61 s.serialize_str(&value.to_string())
62}
63
64fn ser_opt_dec<S: Serializer>(value: &Option<Decimal>, s: S) -> Result<S::Ok, S::Error> {
65 match value {
66 Some(v) => s.serialize_str(&v.to_string()),
67 None => s.serialize_none(),
68 }
69}
70
71#[derive(Debug, Clone, Deserialize)]
73pub struct Envelope<T> {
74 #[serde(rename = "retCode")]
76 pub ret_code: i64,
77 #[serde(rename = "retMsg", default)]
79 pub ret_msg: String,
80 pub result: Option<T>,
82 #[serde(default)]
84 pub time: i64,
85}
86
87#[derive(Debug, Clone, Deserialize)]
89pub struct Page<T> {
90 #[serde(default)]
92 pub category: String,
93 #[serde(default = "Vec::new")]
95 pub list: Vec<T>,
96 #[serde(rename = "nextPageCursor", default)]
98 pub next_page_cursor: String,
99}
100
101#[derive(Debug, Clone, Deserialize)]
103pub struct ServerTime {
104 #[serde(rename = "timeSecond", deserialize_with = "int")]
106 pub time_second: i64,
107 #[serde(rename = "timeNano", deserialize_with = "int")]
109 pub time_nano: i64,
110}
111
112#[derive(Debug, Clone, Deserialize)]
114pub struct PriceFilter {
115 #[serde(rename = "minPrice", deserialize_with = "dec", default)]
117 pub min_price: Decimal,
118 #[serde(rename = "maxPrice", deserialize_with = "dec", default)]
120 pub max_price: Decimal,
121 #[serde(rename = "tickSize", deserialize_with = "dec", default)]
123 pub tick_size: Decimal,
124}
125
126#[derive(Debug, Clone, Deserialize)]
128pub struct LotSizeFilter {
129 #[serde(rename = "maxOrderQty", deserialize_with = "dec", default)]
131 pub max_order_qty: Decimal,
132 #[serde(rename = "maxMktOrderQty", deserialize_with = "dec", default)]
134 pub max_mkt_order_qty: Decimal,
135 #[serde(rename = "minOrderQty", deserialize_with = "dec", default)]
137 pub min_order_qty: Decimal,
138 #[serde(rename = "qtyStep", deserialize_with = "dec", default)]
140 pub qty_step: Decimal,
141 #[serde(rename = "minNotionalValue", deserialize_with = "opt_dec", default)]
143 pub min_notional_value: Option<Decimal>,
144}
145
146#[derive(Debug, Clone, Deserialize)]
148pub struct InstrumentInfo {
149 pub symbol: String,
151 #[serde(rename = "contractType", default)]
153 pub contract_type: String,
154 #[serde(default)]
156 pub status: String,
157 #[serde(rename = "baseCoin", default)]
159 pub base_coin: String,
160 #[serde(rename = "quoteCoin", default)]
162 pub quote_coin: String,
163 #[serde(rename = "settleCoin", default)]
165 pub settle_coin: String,
166 #[serde(rename = "displayName", default)]
168 pub display_name: String,
169 #[serde(rename = "launchTime", deserialize_with = "int", default)]
171 pub launch_time: i64,
172 #[serde(rename = "deliveryTime", deserialize_with = "int", default)]
174 pub delivery_time: i64,
175 #[serde(rename = "priceFilter")]
177 pub price_filter: PriceFilter,
178 #[serde(rename = "lotSizeFilter")]
180 pub lot_size_filter: LotSizeFilter,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
185pub struct Level(
186 #[serde(deserialize_with = "dec")]
188 pub Decimal,
189 #[serde(deserialize_with = "dec")]
191 pub Decimal,
192);
193
194#[derive(Debug, Clone, Deserialize)]
196pub struct OrderBookResult {
197 #[serde(rename = "s", default)]
199 pub symbol: String,
200 #[serde(rename = "b", default)]
202 pub bids: Vec<Level>,
203 #[serde(rename = "a", default)]
205 pub asks: Vec<Level>,
206 #[serde(default)]
208 pub ts: i64,
209 #[serde(rename = "u", default)]
211 pub update_id: i64,
212 #[serde(default)]
214 pub seq: i64,
215 #[serde(default)]
217 pub cts: i64,
218}
219
220#[derive(Debug, Clone, Deserialize)]
222pub struct Ticker {
223 pub symbol: String,
225 #[serde(rename = "lastPrice", deserialize_with = "opt_dec", default)]
227 pub last_price: Option<Decimal>,
228 #[serde(rename = "markPrice", deserialize_with = "opt_dec", default)]
230 pub mark_price: Option<Decimal>,
231 #[serde(rename = "bid1Price", deserialize_with = "opt_dec", default)]
233 pub bid1_price: Option<Decimal>,
234 #[serde(rename = "bid1Size", deserialize_with = "opt_dec", default)]
236 pub bid1_size: Option<Decimal>,
237 #[serde(rename = "ask1Price", deserialize_with = "opt_dec", default)]
239 pub ask1_price: Option<Decimal>,
240 #[serde(rename = "ask1Size", deserialize_with = "opt_dec", default)]
242 pub ask1_size: Option<Decimal>,
243}
244
245#[derive(Debug, Clone, Deserialize)]
247pub struct PublicTradeRecord {
248 #[serde(rename = "execId", default)]
250 pub exec_id: String,
251 pub symbol: String,
253 #[serde(deserialize_with = "dec")]
255 pub price: Decimal,
256 #[serde(deserialize_with = "dec")]
258 pub size: Decimal,
259 pub side: String,
261 #[serde(deserialize_with = "int")]
263 pub time: i64,
264}
265
266#[derive(Debug, Clone, PartialEq)]
268pub struct Kline {
269 pub start_ms: i64,
271 pub open: Decimal,
273 pub high: Decimal,
275 pub low: Decimal,
277 pub close: Decimal,
279 pub volume: Decimal,
281}
282
283impl Kline {
284 pub fn from_row(row: &[String]) -> Option<Self> {
286 let n = |i: usize| row.get(i).and_then(|s| s.trim().parse::<Decimal>().ok());
287 Some(Self {
288 start_ms: row.first()?.trim().parse().ok()?,
289 open: n(1)?,
290 high: n(2)?,
291 low: n(3)?,
292 close: n(4)?,
293 volume: n(5)?,
294 })
295 }
296}
297
298#[derive(Debug, Clone, Deserialize)]
300pub struct KlineResult {
301 #[serde(default)]
303 pub symbol: String,
304 #[serde(default)]
306 pub list: Vec<Vec<String>>,
307}
308
309#[derive(Debug, Clone, Deserialize)]
311pub struct OrderRecord {
312 #[serde(rename = "orderId")]
314 pub order_id: String,
315 #[serde(rename = "orderLinkId", default)]
317 pub order_link_id: String,
318 pub symbol: String,
320 #[serde(deserialize_with = "opt_dec", default)]
322 pub price: Option<Decimal>,
323 #[serde(deserialize_with = "dec", default)]
325 pub qty: Decimal,
326 #[serde(default)]
328 pub side: String,
329 #[serde(rename = "orderStatus", default)]
331 pub order_status: String,
332 #[serde(rename = "orderType", default)]
334 pub order_type: String,
335 #[serde(rename = "stopOrderType", default)]
337 pub stop_order_type: String,
338 #[serde(rename = "cumExecQty", deserialize_with = "dec", default)]
340 pub cum_exec_qty: Decimal,
341 #[serde(rename = "leavesQty", deserialize_with = "opt_dec", default)]
343 pub leaves_qty: Option<Decimal>,
344 #[serde(rename = "avgPrice", deserialize_with = "opt_dec", default)]
346 pub avg_price: Option<Decimal>,
347 #[serde(rename = "triggerPrice", deserialize_with = "opt_dec", default)]
349 pub trigger_price: Option<Decimal>,
350 #[serde(rename = "triggerDirection", deserialize_with = "int", default)]
352 pub trigger_direction: i64,
353 #[serde(rename = "timeInForce", default)]
355 pub time_in_force: String,
356 #[serde(rename = "cancelType", default)]
358 pub cancel_type: String,
359 #[serde(rename = "rejectReason", default)]
361 pub reject_reason: String,
362 #[serde(default)]
364 pub category: String,
365 #[serde(rename = "createdTime", deserialize_with = "int", default)]
367 pub created_time: i64,
368 #[serde(rename = "updatedTime", deserialize_with = "int", default)]
370 pub updated_time: i64,
371}
372
373#[derive(Debug, Clone, Deserialize)]
375pub struct ExecutionRecord {
376 #[serde(rename = "execId")]
378 pub exec_id: String,
379 #[serde(rename = "orderId", default)]
381 pub order_id: String,
382 #[serde(rename = "orderLinkId", default)]
384 pub order_link_id: String,
385 pub symbol: String,
387 #[serde(default)]
389 pub side: String,
390 #[serde(rename = "execPrice", deserialize_with = "dec")]
392 pub exec_price: Decimal,
393 #[serde(rename = "execQty", deserialize_with = "dec")]
395 pub exec_qty: Decimal,
396 #[serde(rename = "execFee", deserialize_with = "dec", default)]
398 pub exec_fee: Decimal,
399 #[serde(rename = "execType", default)]
401 pub exec_type: String,
402 #[serde(rename = "execTime", deserialize_with = "int", default)]
404 pub exec_time: i64,
405 #[serde(default)]
407 pub category: String,
408}
409
410#[derive(Debug, Clone, Deserialize)]
412pub struct PositionRecord {
413 pub symbol: String,
415 #[serde(default)]
417 pub side: String,
418 #[serde(deserialize_with = "dec", default)]
420 pub size: Decimal,
421 #[serde(
423 rename = "avgPrice",
424 alias = "entryPrice",
425 deserialize_with = "opt_dec",
426 default
427 )]
428 pub avg_price: Option<Decimal>,
429 #[serde(rename = "markPrice", deserialize_with = "opt_dec", default)]
431 pub mark_price: Option<Decimal>,
432 #[serde(rename = "unrealisedPnl", deserialize_with = "opt_dec", default)]
434 pub unrealised_pnl: Option<Decimal>,
435 #[serde(rename = "positionIdx", deserialize_with = "int", default)]
437 pub position_idx: i64,
438 #[serde(default)]
440 pub category: String,
441 #[serde(rename = "updatedTime", deserialize_with = "int", default)]
443 pub updated_time: i64,
444}
445
446#[derive(Debug, Clone, Deserialize)]
448pub struct CoinBalance {
449 pub coin: String,
451 #[serde(deserialize_with = "dec", default)]
453 pub equity: Decimal,
454 #[serde(rename = "walletBalance", deserialize_with = "dec", default)]
456 pub wallet_balance: Decimal,
457 #[serde(deserialize_with = "dec", default)]
459 pub locked: Decimal,
460 #[serde(rename = "totalOrderIM", deserialize_with = "dec", default)]
462 pub total_order_im: Decimal,
463 #[serde(rename = "totalPositionIM", deserialize_with = "dec", default)]
465 pub total_position_im: Decimal,
466 #[serde(rename = "unrealisedPnl", deserialize_with = "dec", default)]
468 pub unrealised_pnl: Decimal,
469}
470
471#[derive(Debug, Clone, Deserialize)]
473pub struct WalletRecord {
474 #[serde(rename = "accountType", default)]
476 pub account_type: String,
477 #[serde(rename = "totalEquity", deserialize_with = "dec", default)]
479 pub total_equity: Decimal,
480 #[serde(default)]
482 pub coin: Vec<CoinBalance>,
483}
484
485#[derive(Debug, Clone, Deserialize)]
487pub struct OrderRef {
488 #[serde(rename = "orderId", default)]
490 pub order_id: String,
491 #[serde(rename = "orderLinkId", default)]
493 pub order_link_id: String,
494}
495
496#[derive(Debug, Clone, Deserialize)]
498pub struct CancelAllResult {
499 #[serde(default)]
501 pub list: Vec<OrderRef>,
502}
503
504#[derive(Debug, Clone, Default, Serialize)]
506pub struct CreateOrderRequest {
507 pub category: String,
509 pub symbol: String,
511 pub side: String,
513 #[serde(rename = "orderType")]
515 pub order_type: String,
516 #[serde(serialize_with = "ser_dec")]
518 pub qty: Decimal,
519 #[serde(
521 serialize_with = "ser_opt_dec",
522 skip_serializing_if = "Option::is_none"
523 )]
524 pub price: Option<Decimal>,
525 #[serde(rename = "timeInForce", skip_serializing_if = "Option::is_none")]
527 pub time_in_force: Option<String>,
528 #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
530 pub order_link_id: Option<String>,
531 #[serde(
533 rename = "triggerPrice",
534 serialize_with = "ser_opt_dec",
535 skip_serializing_if = "Option::is_none"
536 )]
537 pub trigger_price: Option<Decimal>,
538 #[serde(rename = "triggerDirection", skip_serializing_if = "Option::is_none")]
540 pub trigger_direction: Option<i64>,
541 #[serde(rename = "triggerBy", skip_serializing_if = "Option::is_none")]
543 pub trigger_by: Option<String>,
544 #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
546 pub reduce_only: Option<bool>,
547 #[serde(rename = "positionIdx", skip_serializing_if = "Option::is_none")]
549 pub position_idx: Option<i64>,
550}
551
552#[derive(Debug, Clone, Default, Serialize)]
554pub struct AmendOrderRequest {
555 pub category: String,
557 pub symbol: String,
559 #[serde(rename = "orderId", skip_serializing_if = "Option::is_none")]
561 pub order_id: Option<String>,
562 #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
564 pub order_link_id: Option<String>,
565 #[serde(
567 serialize_with = "ser_opt_dec",
568 skip_serializing_if = "Option::is_none"
569 )]
570 pub qty: Option<Decimal>,
571 #[serde(
573 serialize_with = "ser_opt_dec",
574 skip_serializing_if = "Option::is_none"
575 )]
576 pub price: Option<Decimal>,
577 #[serde(
579 rename = "triggerPrice",
580 serialize_with = "ser_opt_dec",
581 skip_serializing_if = "Option::is_none"
582 )]
583 pub trigger_price: Option<Decimal>,
584}
585
586#[derive(Debug, Clone, Default, Serialize)]
588pub struct CancelOrderRequest {
589 pub category: String,
591 pub symbol: String,
593 #[serde(rename = "orderId", skip_serializing_if = "Option::is_none")]
595 pub order_id: Option<String>,
596 #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
598 pub order_link_id: Option<String>,
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn numbers_keep_their_digits_and_the_empty_text_is_nothing() {
607 let r: OrderRecord = serde_json::from_str(
608 r#"{"orderId":"1","symbol":"BTCUSDT","price":"81695.30","qty":"0.10","avgPrice":"","triggerPrice":"0.00","triggerDirection":0,"createdTime":"1684738540559"}"#,
609 )
610 .unwrap();
611 assert_eq!(r.price.unwrap().to_string(), "81695.30");
612 assert_eq!(r.qty.to_string(), "0.10");
613 assert_eq!(r.avg_price, None);
614 assert_eq!(r.created_time, 1_684_738_540_559);
615 }
616
617 #[test]
618 fn a_request_sends_numbers_as_texts_and_leaves_out_what_is_not_set() {
619 let body = CreateOrderRequest {
620 category: "linear".into(),
621 symbol: "BTCUSDT".into(),
622 side: "Buy".into(),
623 order_type: "Limit".into(),
624 qty: "0.010".parse().unwrap(),
625 price: Some("25000.5".parse().unwrap()),
626 order_link_id: Some("abc".into()),
627 ..Default::default()
628 };
629 assert_eq!(
630 serde_json::to_string(&body).unwrap(),
631 r#"{"category":"linear","symbol":"BTCUSDT","side":"Buy","orderType":"Limit","qty":"0.010","price":"25000.5","orderLinkId":"abc"}"#
632 );
633 }
634
635 #[test]
636 fn a_kline_row_is_read_and_a_short_row_is_not() {
637 let row: Vec<String> = ["1792760100000", "1", "2", "0.5", "1.5", "6.3", "9"]
638 .iter()
639 .map(|s| s.to_string())
640 .collect();
641 let k = Kline::from_row(&row).unwrap();
642 assert_eq!(
643 (k.start_ms, k.high.to_string()),
644 (1_792_760_100_000, "2".into())
645 );
646 assert!(Kline::from_row(&row[..3]).is_none());
647 }
648}