Skip to main content

brokapi_bybit_protocol/http/
models.rs

1//! Request and response models of the REST endpoints used by the connectors.
2//!
3//! Bybit writes every number as a text (`"81695.30"`) and an absent number as an empty text. The readers here take
4//! either, keep the digits as written (an exact decimal, never a float) and turn the empty text into zero or `None`.
5//! Fields the connectors do not use are not modelled: the reply may carry more.
6
7use rust_decimal::Decimal;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10
11/// A decimal from a JSON text or number. The empty text is zero.
12pub fn dec<'de, D: Deserializer<'de>>(d: D) -> Result<Decimal, D::Error> {
13    Ok(opt_dec(d)?.unwrap_or(Decimal::ZERO))
14}
15
16/// A decimal from a JSON text or number. The empty text, `null` and an absent field are `None`.
17pub 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
39/// An integer from a JSON text or number. The empty text, `null` and an absent field are zero.
40pub 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
59/// Writes a decimal as a JSON text, the way the API wants numbers in a request.
60pub 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/// The envelope of every REST reply.
72#[derive(Debug, Clone, Deserialize)]
73pub struct Envelope<T> {
74    /// `0` is a success.
75    #[serde(rename = "retCode")]
76    pub ret_code: i64,
77    /// The text of the code.
78    #[serde(rename = "retMsg", default)]
79    pub ret_msg: String,
80    /// The payload. Absent or `null` on some refusals.
81    pub result: Option<T>,
82    /// The time of the exchange, milliseconds.
83    #[serde(default)]
84    pub time: i64,
85}
86
87/// A page of a list with a cursor. The list is empty and the cursor empty at the end.
88#[derive(Debug, Clone, Deserialize)]
89pub struct Page<T> {
90    /// The category of the answer.
91    #[serde(default)]
92    pub category: String,
93    /// The rows of the page.
94    #[serde(default = "Vec::new")]
95    pub list: Vec<T>,
96    /// The cursor of the next page, empty at the end.
97    #[serde(rename = "nextPageCursor", default)]
98    pub next_page_cursor: String,
99}
100
101/// The server time (`/v5/market/time`).
102#[derive(Debug, Clone, Deserialize)]
103pub struct ServerTime {
104    /// Seconds since the epoch.
105    #[serde(rename = "timeSecond", deserialize_with = "int")]
106    pub time_second: i64,
107    /// Nanoseconds since the epoch.
108    #[serde(rename = "timeNano", deserialize_with = "int")]
109    pub time_nano: i64,
110}
111
112/// The price rules of an instrument.
113#[derive(Debug, Clone, Deserialize)]
114pub struct PriceFilter {
115    /// The lowest price.
116    #[serde(rename = "minPrice", deserialize_with = "dec", default)]
117    pub min_price: Decimal,
118    /// The highest price.
119    #[serde(rename = "maxPrice", deserialize_with = "dec", default)]
120    pub max_price: Decimal,
121    /// The price step.
122    #[serde(rename = "tickSize", deserialize_with = "dec", default)]
123    pub tick_size: Decimal,
124}
125
126/// The size rules of an instrument.
127#[derive(Debug, Clone, Deserialize)]
128pub struct LotSizeFilter {
129    /// The largest quantity of a limit order.
130    #[serde(rename = "maxOrderQty", deserialize_with = "dec", default)]
131    pub max_order_qty: Decimal,
132    /// The largest quantity of a market order.
133    #[serde(rename = "maxMktOrderQty", deserialize_with = "dec", default)]
134    pub max_mkt_order_qty: Decimal,
135    /// The smallest quantity of an order.
136    #[serde(rename = "minOrderQty", deserialize_with = "dec", default)]
137    pub min_order_qty: Decimal,
138    /// The quantity step.
139    #[serde(rename = "qtyStep", deserialize_with = "dec", default)]
140    pub qty_step: Decimal,
141    /// The smallest value (price times quantity) of an order.
142    #[serde(rename = "minNotionalValue", deserialize_with = "opt_dec", default)]
143    pub min_notional_value: Option<Decimal>,
144}
145
146/// One instrument of `/v5/market/instruments-info`.
147#[derive(Debug, Clone, Deserialize)]
148pub struct InstrumentInfo {
149    /// The symbol, `BTCUSDT`.
150    pub symbol: String,
151    /// `LinearPerpetual`, `LinearFutures`, ...
152    #[serde(rename = "contractType", default)]
153    pub contract_type: String,
154    /// `Trading`, `PreLaunch`, `PendingOpen`, `Delivering`, `Closed`.
155    #[serde(default)]
156    pub status: String,
157    /// The base coin, `BTC`.
158    #[serde(rename = "baseCoin", default)]
159    pub base_coin: String,
160    /// The quote coin, `USDT`.
161    #[serde(rename = "quoteCoin", default)]
162    pub quote_coin: String,
163    /// The settle coin, `USDT`.
164    #[serde(rename = "settleCoin", default)]
165    pub settle_coin: String,
166    /// The name shown on the site.
167    #[serde(rename = "displayName", default)]
168    pub display_name: String,
169    /// When trading starts, milliseconds.
170    #[serde(rename = "launchTime", deserialize_with = "int", default)]
171    pub launch_time: i64,
172    /// When a future is delivered (or a perpetual delisted), milliseconds; `0` for a live perpetual.
173    #[serde(rename = "deliveryTime", deserialize_with = "int", default)]
174    pub delivery_time: i64,
175    /// The price rules.
176    #[serde(rename = "priceFilter")]
177    pub price_filter: PriceFilter,
178    /// The size rules.
179    #[serde(rename = "lotSizeFilter")]
180    pub lot_size_filter: LotSizeFilter,
181}
182
183/// One level of an order book: price and size.
184#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
185pub struct Level(
186    /// The price.
187    #[serde(deserialize_with = "dec")]
188    pub Decimal,
189    /// The size. Zero in a stream delta removes the level.
190    #[serde(deserialize_with = "dec")]
191    pub Decimal,
192);
193
194/// The order book of `/v5/market/orderbook`.
195#[derive(Debug, Clone, Deserialize)]
196pub struct OrderBookResult {
197    /// The symbol.
198    #[serde(rename = "s", default)]
199    pub symbol: String,
200    /// Bids, best first.
201    #[serde(rename = "b", default)]
202    pub bids: Vec<Level>,
203    /// Asks, best first.
204    #[serde(rename = "a", default)]
205    pub asks: Vec<Level>,
206    /// The time the exchange made the data, milliseconds.
207    #[serde(default)]
208    pub ts: i64,
209    /// The update id.
210    #[serde(rename = "u", default)]
211    pub update_id: i64,
212    /// The cross sequence.
213    #[serde(default)]
214    pub seq: i64,
215    /// The time of the matching engine, milliseconds.
216    #[serde(default)]
217    pub cts: i64,
218}
219
220/// One ticker of `/v5/market/tickers`.
221#[derive(Debug, Clone, Deserialize)]
222pub struct Ticker {
223    /// The symbol.
224    pub symbol: String,
225    /// The last price.
226    #[serde(rename = "lastPrice", deserialize_with = "opt_dec", default)]
227    pub last_price: Option<Decimal>,
228    /// The mark price.
229    #[serde(rename = "markPrice", deserialize_with = "opt_dec", default)]
230    pub mark_price: Option<Decimal>,
231    /// The best bid.
232    #[serde(rename = "bid1Price", deserialize_with = "opt_dec", default)]
233    pub bid1_price: Option<Decimal>,
234    /// The size at the best bid.
235    #[serde(rename = "bid1Size", deserialize_with = "opt_dec", default)]
236    pub bid1_size: Option<Decimal>,
237    /// The best ask.
238    #[serde(rename = "ask1Price", deserialize_with = "opt_dec", default)]
239    pub ask1_price: Option<Decimal>,
240    /// The size at the best ask.
241    #[serde(rename = "ask1Size", deserialize_with = "opt_dec", default)]
242    pub ask1_size: Option<Decimal>,
243}
244
245/// One trade of `/v5/market/recent-trade`.
246#[derive(Debug, Clone, Deserialize)]
247pub struct PublicTradeRecord {
248    /// The id of the trade.
249    #[serde(rename = "execId", default)]
250    pub exec_id: String,
251    /// The symbol.
252    pub symbol: String,
253    /// The price.
254    #[serde(deserialize_with = "dec")]
255    pub price: Decimal,
256    /// The size.
257    #[serde(deserialize_with = "dec")]
258    pub size: Decimal,
259    /// The side of the taker, `Buy` or `Sell`.
260    pub side: String,
261    /// The time, milliseconds.
262    #[serde(deserialize_with = "int")]
263    pub time: i64,
264}
265
266/// One candle: the open time and the prices, from the list of `/v5/market/kline`.
267#[derive(Debug, Clone, PartialEq)]
268pub struct Kline {
269    /// The start of the candle, milliseconds.
270    pub start_ms: i64,
271    /// The open price.
272    pub open: Decimal,
273    /// The highest price.
274    pub high: Decimal,
275    /// The lowest price.
276    pub low: Decimal,
277    /// The close price.
278    pub close: Decimal,
279    /// The volume in the base coin.
280    pub volume: Decimal,
281}
282
283impl Kline {
284    /// Reads one row of the list: `[start, open, high, low, close, volume, turnover]`, all texts.
285    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/// The result of `/v5/market/kline`: the rows are the newest first.
299#[derive(Debug, Clone, Deserialize)]
300pub struct KlineResult {
301    /// The symbol.
302    #[serde(default)]
303    pub symbol: String,
304    /// The rows, newest first.
305    #[serde(default)]
306    pub list: Vec<Vec<String>>,
307}
308
309/// An order as the exchange lists it (`/v5/order/realtime`, `/v5/order/history`, and the `order` stream).
310#[derive(Debug, Clone, Deserialize)]
311pub struct OrderRecord {
312    /// The number of the order.
313    #[serde(rename = "orderId")]
314    pub order_id: String,
315    /// The client id (`orderLinkId`), empty when none was given.
316    #[serde(rename = "orderLinkId", default)]
317    pub order_link_id: String,
318    /// The symbol.
319    pub symbol: String,
320    /// The limit price, `None` for a market order.
321    #[serde(deserialize_with = "opt_dec", default)]
322    pub price: Option<Decimal>,
323    /// The quantity.
324    #[serde(deserialize_with = "dec", default)]
325    pub qty: Decimal,
326    /// `Buy` or `Sell`.
327    #[serde(default)]
328    pub side: String,
329    /// `New`, `PartiallyFilled`, `Untriggered`, `Rejected`, `Filled`, `Cancelled`, `Triggered`, `Deactivated`, ...
330    #[serde(rename = "orderStatus", default)]
331    pub order_status: String,
332    /// `Market` or `Limit`.
333    #[serde(rename = "orderType", default)]
334    pub order_type: String,
335    /// `Stop`, `TakeProfit`, `StopLoss`, ... or `UNKNOWN`/empty for a plain order.
336    #[serde(rename = "stopOrderType", default)]
337    pub stop_order_type: String,
338    /// The quantity executed so far.
339    #[serde(rename = "cumExecQty", deserialize_with = "dec", default)]
340    pub cum_exec_qty: Decimal,
341    /// The quantity still working.
342    #[serde(rename = "leavesQty", deserialize_with = "opt_dec", default)]
343    pub leaves_qty: Option<Decimal>,
344    /// The average price of the executed part.
345    #[serde(rename = "avgPrice", deserialize_with = "opt_dec", default)]
346    pub avg_price: Option<Decimal>,
347    /// The trigger price of a conditional order, `None` (or zero) for a plain one.
348    #[serde(rename = "triggerPrice", deserialize_with = "opt_dec", default)]
349    pub trigger_price: Option<Decimal>,
350    /// `1` the order triggers when the price rises to the trigger price, `2` when it falls, `0` not conditional.
351    #[serde(rename = "triggerDirection", deserialize_with = "int", default)]
352    pub trigger_direction: i64,
353    /// `GTC`, `IOC`, `FOK`, `PostOnly`.
354    #[serde(rename = "timeInForce", default)]
355    pub time_in_force: String,
356    /// Why the exchange cancelled it (`CancelByUser`, ...).
357    #[serde(rename = "cancelType", default)]
358    pub cancel_type: String,
359    /// Why the exchange rejected it (`EC_NoError` when it did not).
360    #[serde(rename = "rejectReason", default)]
361    pub reject_reason: String,
362    /// The product category (the streams carry it, the lists put it on the page).
363    #[serde(default)]
364    pub category: String,
365    /// When the order was made, milliseconds.
366    #[serde(rename = "createdTime", deserialize_with = "int", default)]
367    pub created_time: i64,
368    /// When the order last changed, milliseconds.
369    #[serde(rename = "updatedTime", deserialize_with = "int", default)]
370    pub updated_time: i64,
371}
372
373/// A fill (`/v5/execution/list`, and the `execution` stream).
374#[derive(Debug, Clone, Deserialize)]
375pub struct ExecutionRecord {
376    /// The id of the fill.
377    #[serde(rename = "execId")]
378    pub exec_id: String,
379    /// The number of the order.
380    #[serde(rename = "orderId", default)]
381    pub order_id: String,
382    /// The client id of the order.
383    #[serde(rename = "orderLinkId", default)]
384    pub order_link_id: String,
385    /// The symbol.
386    pub symbol: String,
387    /// `Buy` or `Sell`.
388    #[serde(default)]
389    pub side: String,
390    /// The price of the fill.
391    #[serde(rename = "execPrice", deserialize_with = "dec")]
392    pub exec_price: Decimal,
393    /// The quantity of the fill.
394    #[serde(rename = "execQty", deserialize_with = "dec")]
395    pub exec_qty: Decimal,
396    /// The fee. Negative is a rebate.
397    #[serde(rename = "execFee", deserialize_with = "dec", default)]
398    pub exec_fee: Decimal,
399    /// The kind: `Trade`, `AdlTrade`, `Funding`, `BustTrade`, `Delivery`, ...
400    #[serde(rename = "execType", default)]
401    pub exec_type: String,
402    /// When the fill happened, milliseconds.
403    #[serde(rename = "execTime", deserialize_with = "int", default)]
404    pub exec_time: i64,
405    /// The product category (the stream carries it).
406    #[serde(default)]
407    pub category: String,
408}
409
410/// A position of `/v5/position/list` or of the `position` stream.
411#[derive(Debug, Clone, Deserialize)]
412pub struct PositionRecord {
413    /// The symbol.
414    pub symbol: String,
415    /// `Buy` for long, `Sell` for short, empty for no position.
416    #[serde(default)]
417    pub side: String,
418    /// The size, always positive.
419    #[serde(deserialize_with = "dec", default)]
420    pub size: Decimal,
421    /// The average entry price (`avgPrice` in the list, `entryPrice` in the stream).
422    #[serde(
423        rename = "avgPrice",
424        alias = "entryPrice",
425        deserialize_with = "opt_dec",
426        default
427    )]
428    pub avg_price: Option<Decimal>,
429    /// The mark price.
430    #[serde(rename = "markPrice", deserialize_with = "opt_dec", default)]
431    pub mark_price: Option<Decimal>,
432    /// The unrealised profit.
433    #[serde(rename = "unrealisedPnl", deserialize_with = "opt_dec", default)]
434    pub unrealised_pnl: Option<Decimal>,
435    /// `0` one-way mode, `1` long and `2` short side of hedge mode.
436    #[serde(rename = "positionIdx", deserialize_with = "int", default)]
437    pub position_idx: i64,
438    /// The product category (the stream carries it).
439    #[serde(default)]
440    pub category: String,
441    /// When the position last changed, milliseconds.
442    #[serde(rename = "updatedTime", deserialize_with = "int", default)]
443    pub updated_time: i64,
444}
445
446/// One coin of a wallet.
447#[derive(Debug, Clone, Deserialize)]
448pub struct CoinBalance {
449    /// The coin, `USDT`.
450    pub coin: String,
451    /// The equity: the wallet balance plus unrealised profit.
452    #[serde(deserialize_with = "dec", default)]
453    pub equity: Decimal,
454    /// The wallet balance.
455    #[serde(rename = "walletBalance", deserialize_with = "dec", default)]
456    pub wallet_balance: Decimal,
457    /// Locked by open spot orders.
458    #[serde(deserialize_with = "dec", default)]
459    pub locked: Decimal,
460    /// Margin taken by open orders (empty in portfolio margin mode).
461    #[serde(rename = "totalOrderIM", deserialize_with = "dec", default)]
462    pub total_order_im: Decimal,
463    /// Margin taken by positions (empty in portfolio margin mode).
464    #[serde(rename = "totalPositionIM", deserialize_with = "dec", default)]
465    pub total_position_im: Decimal,
466    /// The unrealised profit.
467    #[serde(rename = "unrealisedPnl", deserialize_with = "dec", default)]
468    pub unrealised_pnl: Decimal,
469}
470
471/// A wallet: `/v5/account/wallet-balance` and the `wallet` stream.
472#[derive(Debug, Clone, Deserialize)]
473pub struct WalletRecord {
474    /// `UNIFIED`.
475    #[serde(rename = "accountType", default)]
476    pub account_type: String,
477    /// The equity of the whole account in USD.
478    #[serde(rename = "totalEquity", deserialize_with = "dec", default)]
479    pub total_equity: Decimal,
480    /// The coins.
481    #[serde(default)]
482    pub coin: Vec<CoinBalance>,
483}
484
485/// The answer to a command on one order: the number and the client id.
486#[derive(Debug, Clone, Deserialize)]
487pub struct OrderRef {
488    /// The number of the order.
489    #[serde(rename = "orderId", default)]
490    pub order_id: String,
491    /// The client id.
492    #[serde(rename = "orderLinkId", default)]
493    pub order_link_id: String,
494}
495
496/// The answer to `cancel-all`.
497#[derive(Debug, Clone, Deserialize)]
498pub struct CancelAllResult {
499    /// The orders cancelled.
500    #[serde(default)]
501    pub list: Vec<OrderRef>,
502}
503
504/// The body of `POST /v5/order/create`. Only the fields that are set are sent.
505#[derive(Debug, Clone, Default, Serialize)]
506pub struct CreateOrderRequest {
507    /// The category.
508    pub category: String,
509    /// The symbol.
510    pub symbol: String,
511    /// `Buy` or `Sell`.
512    pub side: String,
513    /// `Market` or `Limit`.
514    #[serde(rename = "orderType")]
515    pub order_type: String,
516    /// The quantity.
517    #[serde(serialize_with = "ser_dec")]
518    pub qty: Decimal,
519    /// The limit price.
520    #[serde(
521        serialize_with = "ser_opt_dec",
522        skip_serializing_if = "Option::is_none"
523    )]
524    pub price: Option<Decimal>,
525    /// `GTC`, `IOC`, `FOK` or `PostOnly`.
526    #[serde(rename = "timeInForce", skip_serializing_if = "Option::is_none")]
527    pub time_in_force: Option<String>,
528    /// The client id, at most 36 characters.
529    #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
530    pub order_link_id: Option<String>,
531    /// The trigger price of a conditional order.
532    #[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    /// `1` rise, `2` fall.
539    #[serde(rename = "triggerDirection", skip_serializing_if = "Option::is_none")]
540    pub trigger_direction: Option<i64>,
541    /// `LastPrice`, `MarkPrice` or `IndexPrice`.
542    #[serde(rename = "triggerBy", skip_serializing_if = "Option::is_none")]
543    pub trigger_by: Option<String>,
544    /// The order can only reduce the position.
545    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
546    pub reduce_only: Option<bool>,
547    /// `0` one-way mode.
548    #[serde(rename = "positionIdx", skip_serializing_if = "Option::is_none")]
549    pub position_idx: Option<i64>,
550}
551
552/// The body of `POST /v5/order/amend`: the order is named by `orderId` or `orderLinkId`, the fields set are changed.
553#[derive(Debug, Clone, Default, Serialize)]
554pub struct AmendOrderRequest {
555    /// The category.
556    pub category: String,
557    /// The symbol.
558    pub symbol: String,
559    /// The number of the order.
560    #[serde(rename = "orderId", skip_serializing_if = "Option::is_none")]
561    pub order_id: Option<String>,
562    /// The client id of the order.
563    #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
564    pub order_link_id: Option<String>,
565    /// The new quantity.
566    #[serde(
567        serialize_with = "ser_opt_dec",
568        skip_serializing_if = "Option::is_none"
569    )]
570    pub qty: Option<Decimal>,
571    /// The new price.
572    #[serde(
573        serialize_with = "ser_opt_dec",
574        skip_serializing_if = "Option::is_none"
575    )]
576    pub price: Option<Decimal>,
577    /// The new trigger price of a conditional order.
578    #[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/// The body of `POST /v5/order/cancel`.
587#[derive(Debug, Clone, Default, Serialize)]
588pub struct CancelOrderRequest {
589    /// The category.
590    pub category: String,
591    /// The symbol.
592    pub symbol: String,
593    /// The number of the order.
594    #[serde(rename = "orderId", skip_serializing_if = "Option::is_none")]
595    pub order_id: Option<String>,
596    /// The client id of the order.
597    #[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}