1use std::collections::BTreeMap;
4use std::time::Instant;
5
6use brokapi_core::{
7 BookLevel, Candle, Error, InstrumentId, MarketTrade, OrderBook, Price, Qty, Quote, Side,
8 Timeframe, UnixNanos,
9};
10use brokapi_finam_protocol::pb::google::r#type::Decimal;
11use brokapi_finam_protocol::pb::grpc::tradeapi::v1::Side as PbSide;
12use brokapi_finam_protocol::pb::grpc::tradeapi::v1::marketdata as pb;
13use brokapi_finam_protocol::pb::grpc::tradeapi::v1::marketdata::{
14 TimeFrame, order_book, stream_order_book,
15};
16use chrono::{DateTime, Datelike, Months, TimeZone, Utc};
17
18use crate::error::unsupported;
19use crate::util::{price, price0, qty0, timestamps, ts_nanos};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RowAction {
24 Remove,
26 Set,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct RowView {
33 pub price: Price,
35 pub is_ask: bool,
37 pub size: Qty,
39 pub action: RowAction,
41 pub ts: Option<UnixNanos>,
43}
44
45pub fn rest_rows(rows: &[order_book::Row]) -> Vec<RowView> {
47 rows.iter()
48 .filter_map(|r| {
49 let (is_ask, size) = match &r.side {
50 Some(order_book::row::Side::SellSize(d)) => (true, qty0(&Some(d.clone()))),
51 Some(order_book::row::Side::BuySize(d)) => (false, qty0(&Some(d.clone()))),
52 None => return None,
53 };
54 Some(RowView {
55 price: price(&r.price)?,
56 is_ask,
57 size,
58 action: if r.action == order_book::row::Action::Remove as i32 {
59 RowAction::Remove
60 } else {
61 RowAction::Set
62 },
63 ts: r.timestamp.as_ref().map(ts_nanos),
64 })
65 })
66 .collect()
67}
68
69pub fn stream_rows(rows: &[stream_order_book::Row]) -> Vec<RowView> {
71 rows.iter()
72 .filter_map(|r| {
73 let (is_ask, size) = match &r.side {
74 Some(stream_order_book::row::Side::SellSize(d)) => (true, qty0(&Some(d.clone()))),
75 Some(stream_order_book::row::Side::BuySize(d)) => (false, qty0(&Some(d.clone()))),
76 None => return None,
77 };
78 Some(RowView {
79 price: price(&r.price)?,
80 is_ask,
81 size,
82 action: if r.action == stream_order_book::row::Action::Remove as i32 {
83 RowAction::Remove
84 } else {
85 RowAction::Set
86 },
87 ts: r.timestamp.as_ref().map(ts_nanos),
88 })
89 })
90 .collect()
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum ApplyOutcome {
96 Applied,
98 NeedSnapshot,
100}
101
102#[derive(Debug, Clone, Default)]
135pub struct BookState {
136 bids: BTreeMap<Price, Qty>,
137 asks: BTreeMap<Price, Qty>,
138 have_snapshot: bool,
139}
140
141impl BookState {
142 pub fn reset(&mut self) {
144 *self = Self::default();
145 }
146
147 pub fn has_snapshot(&self) -> bool {
149 self.have_snapshot
150 }
151
152 pub fn apply(&mut self, rows: &[RowView], is_snapshot: bool) -> ApplyOutcome {
156 if is_snapshot {
157 self.bids.clear();
158 self.asks.clear();
159 self.have_snapshot = true;
160 } else if !self.have_snapshot {
161 return ApplyOutcome::NeedSnapshot;
162 }
163 for r in rows {
164 let side = if r.is_ask {
165 &mut self.asks
166 } else {
167 &mut self.bids
168 };
169 let key = r.price;
170 match r.action {
171 RowAction::Remove => {
172 side.remove(&key);
173 }
174 RowAction::Set if r.size.is_positive() => {
175 side.insert(key, r.size);
176 }
177 RowAction::Set => {
179 side.remove(&key);
180 }
181 }
182 }
183 ApplyOutcome::Applied
184 }
185
186 pub fn levels(&self, depth: usize) -> (Vec<BookLevel>, Vec<BookLevel>) {
188 let bids = self
189 .bids
190 .iter()
191 .rev()
192 .take(depth)
193 .map(|(p, q)| level(*p, *q))
194 .collect();
195 let asks = self
196 .asks
197 .iter()
198 .take(depth)
199 .map(|(p, q)| level(*p, *q))
200 .collect();
201 (bids, asks)
202 }
203}
204
205fn level(price: Price, qty: Qty) -> BookLevel {
206 BookLevel { price, qty }
207}
208
209pub fn book_snapshot(
211 instrument: &InstrumentId,
212 state: &BookState,
213 depth: usize,
214 broker_ts: Option<UnixNanos>,
215 now: Instant,
216) -> OrderBook {
217 let (bids, asks) = state.levels(depth);
218 let mut ts = timestamps(None, now);
219 ts.broker_ts = broker_ts;
220 OrderBook::Snapshot {
221 instrument: instrument.clone(),
222 bids,
223 asks,
224 ts,
225 }
226}
227
228pub fn map_rest_order_book(
230 instrument: &InstrumentId,
231 resp: &pb::OrderBookResponse,
232 depth: u32,
233 now: Instant,
234) -> OrderBook {
235 let rows = resp
236 .orderbook
237 .as_ref()
238 .map(|b| rest_rows(&b.rows))
239 .unwrap_or_default();
240 let mut state = BookState::default();
241 state.apply(&rows, true);
242 let broker_ts = rows.iter().filter_map(|r| r.ts).max();
243 book_snapshot(instrument, &state, depth as usize, broker_ts, now)
244}
245
246pub fn map_quote(instrument: &InstrumentId, q: &pb::Quote, now: Instant) -> Quote {
248 let side = |price: &Option<Decimal>, size: &Option<Decimal>| {
249 let p = price0(price);
250 p.is_positive().then(|| level(p, qty0(size)))
251 };
252 Quote {
253 instrument: instrument.clone(),
254 bid: side(&q.bid, &q.bid_size),
255 ask: side(&q.ask, &q.ask_size),
256 ts: timestamps(q.timestamp.as_ref(), now),
257 }
258}
259
260pub fn map_trade(instrument: &InstrumentId, t: &pb::Trade, now: Instant) -> Option<MarketTrade> {
263 let side = match PbSide::try_from(t.side).unwrap_or(PbSide::Unspecified) {
264 PbSide::Buy => Side::Buy,
265 PbSide::Sell => Side::Sell,
266 PbSide::Unspecified => return None,
267 };
268 Some(MarketTrade {
269 instrument: instrument.clone(),
270 price: price0(&t.price),
271 qty: qty0(&t.size),
272 side,
273 trade_id: (!t.trade_id.is_empty()).then(|| t.trade_id.clone()),
274 ts: timestamps(t.timestamp.as_ref(), now),
275 })
276}
277
278pub fn timeframe_to_pb(tf: Timeframe) -> Result<TimeFrame, Error> {
280 Ok(match tf {
281 Timeframe::M1 => TimeFrame::M1,
282 Timeframe::M5 => TimeFrame::M5,
283 Timeframe::M15 => TimeFrame::M15,
284 Timeframe::M30 => TimeFrame::M30,
285 Timeframe::H1 => TimeFrame::H1,
286 Timeframe::H4 => TimeFrame::H4,
287 Timeframe::D1 => TimeFrame::D,
288 Timeframe::W1 => TimeFrame::W,
289 Timeframe::MN => TimeFrame::Mn,
290 Timeframe::M10 => return Err(unsupported("timeframe_m10")),
291 })
292}
293
294pub fn wire_timeframe(tf: Timeframe) -> Result<TimeFrame, Error> {
297 match tf {
298 Timeframe::M10 => Ok(TimeFrame::M5),
299 other => timeframe_to_pb(other),
300 }
301}
302
303pub fn bar_close(tf: Timeframe, open: UnixNanos) -> UnixNanos {
305 const SEC: i64 = 1_000_000_000;
306 let fixed = |secs: i64| UnixNanos::from_nanos(open.as_nanos() + secs * SEC);
307 match tf {
308 Timeframe::M1 => fixed(60),
309 Timeframe::M5 => fixed(300),
310 Timeframe::M10 => fixed(600),
311 Timeframe::M15 => fixed(900),
312 Timeframe::M30 => fixed(1800),
313 Timeframe::H1 => fixed(3600),
314 Timeframe::H4 => fixed(4 * 3600),
315 Timeframe::D1 => fixed(86_400),
316 Timeframe::W1 => fixed(7 * 86_400),
317 Timeframe::MN => {
318 let secs = open.as_nanos().div_euclid(SEC);
319 let dt: DateTime<Utc> = Utc.timestamp_opt(secs, 0).single().unwrap_or_default();
320 let next = dt
321 .checked_add_months(Months::new(1))
322 .map(|d| d.with_day(dt.day()).unwrap_or(d))
323 .unwrap_or(dt);
324 UnixNanos::from_nanos(next.timestamp() * SEC)
325 }
326 }
327}
328
329pub fn map_bar(instrument: &InstrumentId, tf: Timeframe, b: &pb::Bar, now: Instant) -> Candle {
333 let open_ts = b
334 .timestamp
335 .as_ref()
336 .map(ts_nanos)
337 .unwrap_or(UnixNanos::ZERO);
338 Candle {
339 instrument: instrument.clone(),
340 timeframe: tf,
341 open: price0(&b.open),
342 high: price0(&b.high),
343 low: price0(&b.low),
344 close: price0(&b.close),
345 volume: qty0(&b.volume),
346 open_ts,
347 close_ts: bar_close(tf, open_ts),
348 ts: timestamps(b.timestamp.as_ref(), now),
349 }
350}