1use std::collections::{HashMap, HashSet, VecDeque};
4use std::time::Instant;
5
6use brokapi_core::{Event, InstrumentId, ResyncReason, SubscriptionId, Timeframe};
7use brokapi_finam_protocol::pb::grpc::tradeapi::v1::marketdata as pb;
8use brokapi_sansio::{
9 BrokerCore, CoreEvent, DesiredSubscription, EventSink, Outbound, SubscriptionKind,
10 SubscriptionSet,
11};
12
13use super::{StreamEvent, SubStatus};
14use crate::map::m10::M10Builder;
15use crate::map::market::{
16 ApplyOutcome, BookState, book_snapshot, map_bar, map_quote, map_trade, stream_rows,
17};
18
19pub const DEFAULT_DEPTH: u32 = 50;
21
22#[derive(Debug, Clone)]
24pub enum MdEvent {
25 OrderBook(pb::SubscribeOrderBookResponse),
27 Quotes(pb::SubscribeQuoteResponse),
29 Trades(pb::SubscribeLatestTradesResponse),
31 Bars(pb::SubscribeBarsResponse),
33 Connected,
35 Disconnected(String),
37 Failed(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct MdFrame {
44 pub sub: SubscriptionId,
46 pub event: MdEvent,
48}
49
50impl MdFrame {
51 pub fn lifecycle(sub: SubscriptionId, ev: StreamEvent<()>) -> Option<Self> {
67 let event = match ev {
68 StreamEvent::Connected => MdEvent::Connected,
69 StreamEvent::Disconnected(r) => MdEvent::Disconnected(r),
70 StreamEvent::Failed(r) => MdEvent::Failed(r),
71 StreamEvent::Msg(()) => return None,
72 };
73 Some(Self { sub, event })
74 }
75}
76
77#[derive(Debug, Default)]
78struct BookEntry {
79 state: BookState,
80 connects: u32,
82}
83
84#[derive(Debug, Default)]
85struct SeenTrades {
86 ids: HashSet<String>,
87 order: VecDeque<String>,
88}
89
90impl SeenTrades {
91 const CAP: usize = 2048;
92
93 fn first_time(&mut self, id: &str) -> bool {
95 if id.is_empty() {
96 return true;
97 }
98 if !self.ids.insert(id.to_string()) {
99 return false;
100 }
101 self.order.push_back(id.to_string());
102 if self.order.len() > Self::CAP
103 && let Some(old) = self.order.pop_front()
104 {
105 self.ids.remove(&old);
106 }
107 true
108 }
109}
110
111#[derive(Debug, Default)]
119pub struct MdCore {
120 desired: SubscriptionSet,
121 status: HashMap<SubscriptionId, SubStatus>,
122 books: HashMap<SubscriptionId, BookEntry>,
123 seen_trades: HashMap<SubscriptionId, SeenTrades>,
124 m10: HashMap<SubscriptionId, M10Builder>,
126 restarts: Vec<SubscriptionId>,
128}
129
130impl MdCore {
131 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn desired(&self) -> &SubscriptionSet {
138 &self.desired
139 }
140
141 pub fn status(&self, sub: SubscriptionId) -> Option<SubStatus> {
143 self.status.get(&sub).cloned()
144 }
145
146 pub fn take_restarts(&mut self) -> Vec<SubscriptionId> {
148 std::mem::take(&mut self.restarts)
149 }
150
151 fn kind(&self, sub: SubscriptionId) -> Option<&SubscriptionKind> {
152 self.desired.find(sub).map(|d| &d.kind)
153 }
154
155 fn on_order_book(
156 &mut self,
157 sub: SubscriptionId,
158 instrument: InstrumentId,
159 depth: u32,
160 msg: pb::StreamOrderBook,
161 now: Instant,
162 out: &mut EventSink,
163 ) {
164 let rows = stream_rows(&msg.rows);
165 let entry = self.books.entry(sub).or_default();
166 match entry.state.apply(&rows, msg.is_data_snapshot) {
167 ApplyOutcome::NeedSnapshot => {
168 self.restarts.push(sub);
171 out.push(CoreEvent {
172 sub: Some(sub),
173 payload: Event::Resync {
174 sub,
175 instrument,
176 reason: ResyncReason::SequenceGap,
177 },
178 });
179 }
180 ApplyOutcome::Applied => {
181 let broker_ts = rows.iter().filter_map(|r| r.ts).max();
182 let book = book_snapshot(&instrument, &entry.state, depth as usize, broker_ts, now);
183 out.push(CoreEvent {
184 sub: Some(sub),
185 payload: Event::OrderBook(book),
186 });
187 }
188 }
189 }
190}
191
192impl BrokerCore for MdCore {
193 type Frame = MdFrame;
194
195 fn on_frame(&mut self, frame: MdFrame, now: Instant, out: &mut EventSink) {
196 let MdFrame { sub, event } = frame;
197 let Some(kind) = self.kind(sub).cloned() else {
198 return;
200 };
201 match event {
202 MdEvent::Connected => {
203 self.status.insert(sub, SubStatus::Active);
204 if let SubscriptionKind::OrderBook { instrument, .. } = &kind {
205 let entry = self.books.entry(sub).or_default();
207 let reopened = entry.connects > 0;
208 entry.state.reset();
209 entry.connects += 1;
210 if reopened {
211 out.push(CoreEvent {
212 sub: Some(sub),
213 payload: Event::Resync {
214 sub,
215 instrument: instrument.clone(),
216 reason: ResyncReason::SubscriptionReset,
217 },
218 });
219 }
220 }
221 }
222 MdEvent::Disconnected(r) => {
223 self.status.insert(sub, SubStatus::Reconnecting(r));
224 }
225 MdEvent::Failed(r) => {
226 self.status.insert(sub, SubStatus::Failed(r));
227 }
228 MdEvent::OrderBook(msg) => {
229 if let SubscriptionKind::OrderBook { instrument, depth } = kind {
230 self.status.insert(sub, SubStatus::Active);
231 let symbol = format!("{}@{}", instrument.key.symbol, instrument.key.mic);
232 for book in msg.order_book {
233 if !book.symbol.is_empty() && book.symbol != symbol {
234 continue;
235 }
236 self.on_order_book(sub, instrument.clone(), depth, book, now, out);
237 }
238 }
239 }
240 MdEvent::Quotes(msg) => {
241 if let SubscriptionKind::Quotes { instrument } = kind {
242 if let Some(e) = &msg.error {
243 self.status.insert(
244 sub,
245 SubStatus::Erroring(format!("{}: {}", e.code, e.description)),
246 );
247 return;
248 }
249 self.status.insert(sub, SubStatus::Active);
250 let symbol = format!("{}@{}", instrument.key.symbol, instrument.key.mic);
251 for q in &msg.quote {
252 if !q.symbol.is_empty() && q.symbol != symbol {
254 continue;
255 }
256 out.push(CoreEvent {
257 sub: Some(sub),
258 payload: Event::Quote(map_quote(&instrument, q, now)),
259 });
260 }
261 }
262 }
263 MdEvent::Trades(msg) => {
264 if let SubscriptionKind::LastTrades { instrument } = kind {
265 self.status.insert(sub, SubStatus::Active);
266 let seen = self.seen_trades.entry(sub).or_default();
267 for t in &msg.trades {
268 if !seen.first_time(&t.trade_id) {
269 continue;
270 }
271 if let Some(trade) = map_trade(&instrument, t, now) {
272 out.push(CoreEvent {
273 sub: Some(sub),
274 payload: Event::MarketTrade(trade),
275 });
276 }
277 }
278 }
279 }
280 MdEvent::Bars(msg) => {
281 if let SubscriptionKind::LastCandle {
282 instrument,
283 timeframe,
284 } = kind
285 {
286 self.status.insert(sub, SubStatus::Active);
287 for b in &msg.bars {
288 let candle = if timeframe == Timeframe::M10 {
289 let m5 = map_bar(&instrument, Timeframe::M5, b, now);
291 match self.m10.entry(sub).or_default().on_m5(&m5) {
292 Some(c) => c,
293 None => continue,
294 }
295 } else {
296 map_bar(&instrument, timeframe, b, now)
297 };
298 out.push(CoreEvent {
299 sub: Some(sub),
300 payload: Event::Candle(candle),
301 });
302 }
303 }
304 }
305 }
306 }
307
308 fn poll_transmit(&mut self) -> Option<Outbound> {
310 None
311 }
312
313 fn poll_timeout(&self) -> Option<Instant> {
314 None
315 }
316
317 fn on_timeout(&mut self, _now: Instant, _out: &mut EventSink) {}
318
319 fn apply(&mut self, desired: &SubscriptionSet) {
320 let keep: HashSet<SubscriptionId> = desired.iter().map(|d| d.id).collect();
321 self.status.retain(|id, _| keep.contains(id));
322 self.books.retain(|id, _| keep.contains(id));
323 self.seen_trades.retain(|id, _| keep.contains(id));
324 self.m10.retain(|id, _| keep.contains(id));
325 for DesiredSubscription { id, kind } in desired.iter() {
326 self.status.entry(*id).or_insert(SubStatus::Connecting);
327 if matches!(kind, SubscriptionKind::OrderBook { .. }) {
328 self.books.entry(*id).or_default();
329 }
330 }
331 self.desired = desired.clone();
332 }
333}