1use 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
12pub 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
23pub 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
39pub 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
61pub 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
77pub 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}