Skip to main content

brokapi_bybit/map/
market.rs

1//! Market data: books, quotes, trades and candles.
2
3use std::time::Instant;
4
5use brokapi_bybit_protocol::http::models::{Kline, Level, PublicTradeRecord};
6use brokapi_core::{
7    BookLevel, Candle, InstrumentId, MarketTrade, OrderBook, Price, Qty, Quote, Timeframe,
8};
9
10use super::{broker_ts, ms_to_nanos, side_of, timestamps};
11
12/// The levels of one side, as the library spells them.
13pub fn levels(levels: &[(brokapi_core::Decimal, brokapi_core::Decimal)]) -> Vec<BookLevel> {
14    levels
15        .iter()
16        .map(|(p, q)| BookLevel {
17            price: Price::new(*p),
18            qty: Qty::new(*q),
19        })
20        .collect()
21}
22
23/// A snapshot of the book from the REST answer.
24pub fn order_book(
25    bids: &[(brokapi_core::Decimal, brokapi_core::Decimal)],
26    asks: &[(brokapi_core::Decimal, brokapi_core::Decimal)],
27    ts_ms: i64,
28    instrument: &InstrumentId,
29    now: Instant,
30) -> OrderBook {
31    OrderBook::Snapshot {
32        instrument: instrument.clone(),
33        bids: levels(bids),
34        asks: levels(asks),
35        ts: timestamps(now, broker_ts(ts_ms)),
36    }
37}
38
39/// The best bid and ask from the first level of each side.
40pub fn quote(
41    bid: Option<&Level>,
42    ask: Option<&Level>,
43    ts_ms: i64,
44    instrument: &InstrumentId,
45    now: Instant,
46) -> Quote {
47    let level = |l: Option<&Level>| {
48        l.filter(|l| !l.0.is_zero()).map(|l| BookLevel {
49            price: Price::new(l.0),
50            qty: Qty::new(l.1),
51        })
52    };
53    Quote {
54        instrument: instrument.clone(),
55        bid: level(bid),
56        ask: level(ask),
57        ts: timestamps(now, broker_ts(ts_ms)),
58    }
59}
60
61/// A public trade of the REST answer.
62pub fn market_trade(
63    r: &PublicTradeRecord,
64    instrument: &InstrumentId,
65    now: Instant,
66) -> Option<MarketTrade> {
67    Some(MarketTrade {
68        instrument: instrument.clone(),
69        price: Price::new(r.price),
70        qty: Qty::new(r.size),
71        side: side_of(&r.side)?,
72        trade_id: (!r.exec_id.is_empty()).then(|| r.exec_id.clone()),
73        ts: timestamps(now, broker_ts(r.time)),
74    })
75}
76
77/// A candle from a row of the history. The candle closes one timeframe after it opens.
78pub fn candle(k: &Kline, instrument: &InstrumentId, tf: Timeframe, now: Instant) -> Candle {
79    let open_ts = ms_to_nanos(k.start_ms);
80    let close_ts = ms_to_nanos(k.start_ms.saturating_add(tf.seconds() as i64 * 1000));
81    Candle {
82        instrument: instrument.clone(),
83        timeframe: tf,
84        open: Price::new(k.open),
85        high: Price::new(k.high),
86        low: Price::new(k.low),
87        close: Price::new(k.close),
88        volume: Qty::new(k.volume),
89        open_ts,
90        close_ts,
91        ts: timestamps(now, Some(open_ts)),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::util::instrument_id;
99    use brokapi_core::{Decimal, dec};
100
101    fn btc() -> InstrumentId {
102        instrument_id("BTCUSDT", "linear")
103    }
104
105    #[test]
106    fn a_book_keeps_the_digits_of_the_prices() {
107        let book = order_book(
108            &[(dec!(81695.20), dec!(0.699))],
109            &[(dec!(81695.30), dec!(7.638))],
110            1_792_760_139_559,
111            &btc(),
112            Instant::now(),
113        );
114        let OrderBook::Snapshot { bids, asks, ts, .. } = book else {
115            panic!("a snapshot");
116        };
117        assert_eq!(bids[0].price.to_string(), "81695.20");
118        assert_eq!(asks[0].qty.to_string(), "7.638");
119        assert_eq!(ts.broker_ts.unwrap().as_nanos(), 1_792_760_139_559_000_000);
120    }
121
122    #[test]
123    fn a_candle_closes_one_timeframe_after_it_opens() {
124        let k = Kline {
125            start_ms: 1_700_000_000_000,
126            open: dec!(1),
127            high: dec!(2),
128            low: dec!(1),
129            close: dec!(2),
130            volume: dec!(5),
131        };
132        let c = candle(&k, &btc(), Timeframe::M5, Instant::now());
133        assert_eq!(
134            c.close_ts.as_nanos() - c.open_ts.as_nanos(),
135            300_000_000_000
136        );
137    }
138
139    #[test]
140    fn an_empty_side_is_no_quote_level() {
141        let q = quote(
142            None,
143            Some(&Level(dec!(2), dec!(1))),
144            0,
145            &btc(),
146            Instant::now(),
147        );
148        assert!(q.bid.is_none());
149        assert_eq!(q.ask.unwrap().price.to_string(), "2");
150    }
151}