1use std::collections::{HashMap, VecDeque};
12use std::time::{Duration, Instant};
13
14use brokapi_core::{Event, InstrumentId, ResyncReason, SubscriptionId, Timeframe};
15use brokapi_sansio::{
16 BrokerCore, DesiredSubscription, EventSink, Outbound, SubscriptionKind, SubscriptionSet,
17};
18use brokapi_tbank_protocol::v1 as pb;
19use brokapi_tbank_protocol::v1::market_data_request::Payload as Req;
20use brokapi_tbank_protocol::v1::market_data_response::Payload as Resp;
21
22use crate::ids::{instrument_ref, matches};
23use crate::map::market;
24
25pub const RETRY_AFTER: Duration = Duration::from_secs(5);
28
29#[allow(clippy::large_enum_variant)]
32#[derive(Debug, Clone)]
33pub enum MdFrame {
34 Connected,
36 Response(pb::MarketDataResponse),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SubUpdate {
46 pub sub: SubscriptionId,
48 pub result: Result<(), String>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum LState {
56 Pending,
58 Active,
60 Failed,
62}
63
64#[derive(Debug, Clone, Hash, PartialEq, Eq)]
65enum WireKey {
66 Book(String),
67 Trades(String),
68 Candles(String, i32),
69}
70
71#[derive(Debug, Clone)]
72enum WireState {
73 Pending,
74 Active,
75 Failed { retry_at: Option<Instant> },
76}
77
78#[derive(Debug, Clone)]
79struct Wire {
80 instrument: InstrumentId,
81 depth: u32,
83 state: WireState,
84}
85
86#[derive(Debug, Clone)]
87enum Kind {
88 Book { depth: u32 },
89 Quotes,
90 Trades,
91 Candles(Timeframe),
92}
93
94#[derive(Debug, Clone)]
95struct Logical {
96 instrument: InstrumentId,
97 kind: Kind,
98 wire: WireKey,
99 state: LState,
100}
101
102#[derive(Debug)]
110pub struct MdCore {
111 desired: SubscriptionSet,
112 logical: HashMap<SubscriptionId, Logical>,
113 wires: HashMap<WireKey, Wire>,
114 requests: VecDeque<pb::MarketDataRequest>,
115 updates: Vec<SubUpdate>,
116 unsubscribing: Vec<(WireKey, InstrumentId)>,
118 unsub_ok: usize,
119 unsub_failed: usize,
120 connections: u32,
121 ping_ms: i32,
122}
123
124impl Default for MdCore {
125 fn default() -> Self {
126 Self::new(Duration::from_secs(30))
127 }
128}
129
130fn wire_key(instrument: &InstrumentId, kind: &Kind) -> WireKey {
131 let r = instrument_ref(instrument);
132 match kind {
133 Kind::Book { .. } | Kind::Quotes => WireKey::Book(r),
134 Kind::Trades => WireKey::Trades(r),
135 Kind::Candles(tf) => WireKey::Candles(r, market::subscription_interval(*tf) as i32),
136 }
137}
138
139impl MdCore {
140 pub fn new(ping_interval: Duration) -> Self {
142 Self {
143 desired: SubscriptionSet::new(),
144 logical: HashMap::new(),
145 wires: HashMap::new(),
146 requests: VecDeque::new(),
147 updates: Vec::new(),
148 unsubscribing: Vec::new(),
149 unsub_ok: 0,
150 unsub_failed: 0,
151 connections: 0,
152 ping_ms: ping_interval.as_millis().clamp(5_000, 180_000) as i32,
153 }
154 }
155
156 pub fn poll_request(&mut self) -> Option<pb::MarketDataRequest> {
158 self.requests.pop_front()
159 }
160
161 pub fn take_updates(&mut self) -> Vec<SubUpdate> {
163 std::mem::take(&mut self.updates)
164 }
165
166 pub fn desired(&self) -> &SubscriptionSet {
168 &self.desired
169 }
170
171 pub fn state(&self, sub: SubscriptionId) -> Option<LState> {
173 self.logical.get(&sub).map(|l| l.state)
174 }
175
176 pub fn wire_count(&self) -> usize {
178 self.wires.len()
179 }
180
181 pub fn unsubscribe_progress(&self) -> (usize, usize, usize) {
183 (self.unsub_ok, self.unsub_failed, self.unsubscribing.len())
184 }
185
186 pub fn give_up_unsubscribing(&mut self) {
188 self.unsub_failed += self.unsubscribing.len();
189 self.unsubscribing.clear();
190 }
191
192 fn instruments_request(
193 key: &WireKey,
194 w: &Wire,
195 action: pb::SubscriptionAction,
196 ) -> pb::MarketDataRequest {
197 let payload = match key {
198 WireKey::Book(r) => Req::SubscribeOrderBookRequest(pb::SubscribeOrderBookRequest {
199 subscription_action: action as i32,
200 instruments: vec![pb::OrderBookInstrument {
201 depth: w.depth as i32,
202 instrument_id: r.clone(),
203 ..Default::default()
204 }],
205 }),
206 WireKey::Trades(r) => Req::SubscribeTradesRequest(pb::SubscribeTradesRequest {
207 subscription_action: action as i32,
208 instruments: vec![pb::TradeInstrument {
209 instrument_id: r.clone(),
210 ..Default::default()
211 }],
212 ..Default::default()
213 }),
214 WireKey::Candles(r, interval) => {
215 Req::SubscribeCandlesRequest(pb::SubscribeCandlesRequest {
216 subscription_action: action as i32,
217 instruments: vec![pb::CandleInstrument {
218 interval: *interval,
219 instrument_id: r.clone(),
220 ..Default::default()
221 }],
222 waiting_close: false,
223 candle_source_type: None,
224 })
225 }
226 };
227 pb::MarketDataRequest {
228 payload: Some(payload),
229 }
230 }
231
232 fn queue(&mut self, key: &WireKey, action: pb::SubscriptionAction) {
233 if let Some(w) = self.wires.get(key) {
234 let req = Self::instruments_request(key, w, action);
235 self.requests.push_back(req);
236 }
237 }
238
239 fn mark_active(&mut self, key: &WireKey) {
240 if let Some(w) = self.wires.get_mut(key) {
241 w.state = WireState::Active;
242 }
243 for (id, l) in self.logical.iter_mut() {
244 if &l.wire == key && l.state != LState::Active {
245 l.state = LState::Active;
246 self.updates.push(SubUpdate {
247 sub: *id,
248 result: Ok(()),
249 });
250 }
251 }
252 }
253
254 fn mark_failed(&mut self, key: &WireKey, status: pb::SubscriptionStatus, now: Instant) {
255 let transient = matches!(
256 status,
257 pb::SubscriptionStatus::InternalError | pb::SubscriptionStatus::TooManyRequests
258 );
259 if let Some(w) = self.wires.get_mut(key) {
260 w.state = WireState::Failed {
261 retry_at: transient.then_some(now + RETRY_AFTER),
262 };
263 }
264 for (id, l) in self.logical.iter_mut() {
265 if &l.wire == key && l.state == LState::Pending {
266 l.state = LState::Failed;
267 self.updates.push(SubUpdate {
268 sub: *id,
269 result: Err(status.as_str_name().to_string()),
270 });
271 }
272 }
273 }
274
275 fn sync_wires(&mut self) {
277 let mut needed: HashMap<WireKey, (InstrumentId, u32)> = HashMap::new();
278 for l in self.logical.values() {
279 let depth = match l.kind {
280 Kind::Book { depth } => market::wire_depth(depth),
281 Kind::Quotes => 1,
282 _ => 0,
283 };
284 let e = needed
285 .entry(l.wire.clone())
286 .or_insert_with(|| (l.instrument.clone(), 0));
287 e.1 = e.1.max(depth);
288 }
289
290 let stale: Vec<WireKey> = self
291 .wires
292 .keys()
293 .filter(|k| !needed.contains_key(*k))
294 .cloned()
295 .collect();
296 for key in stale {
297 self.queue(&key, pb::SubscriptionAction::Unsubscribe);
298 if let Some(w) = self.wires.remove(&key) {
299 self.unsubscribing.push((key, w.instrument));
300 }
301 }
302
303 for (key, (instrument, depth)) in needed {
304 match self.wires.get(&key).map(|w| w.depth) {
305 None => {
306 self.wires.insert(
307 key.clone(),
308 Wire {
309 instrument,
310 depth,
311 state: WireState::Pending,
312 },
313 );
314 self.queue(&key, pb::SubscriptionAction::Subscribe);
315 }
316 Some(old) if old != depth => {
317 self.queue(&key, pb::SubscriptionAction::Unsubscribe);
319 if let Some(w) = self.wires.get_mut(&key) {
320 w.depth = depth;
321 w.state = WireState::Pending;
322 }
323 self.queue(&key, pb::SubscriptionAction::Subscribe);
324 }
325 Some(_) => {}
326 }
327 }
328
329 let ready: Vec<(SubscriptionId, bool)> = self
331 .logical
332 .iter()
333 .filter(|(_, l)| l.state == LState::Pending)
334 .filter_map(|(id, l)| match self.wires.get(&l.wire).map(|w| &w.state) {
335 Some(WireState::Active) => Some((*id, true)),
336 _ => None,
337 })
338 .collect();
339 for (id, _) in ready {
340 if let Some(l) = self.logical.get_mut(&id) {
341 l.state = LState::Active;
342 }
343 self.updates.push(SubUpdate {
344 sub: id,
345 result: Ok(()),
346 });
347 }
348 }
349
350 fn on_connected(&mut self, out: &mut EventSink) {
351 self.connections += 1;
352 self.requests.clear();
353 self.unsubscribing.clear();
354 self.requests.push_back(pb::MarketDataRequest {
355 payload: Some(Req::PingSettings(pb::PingDelaySettings {
356 ping_delay_ms: Some(self.ping_ms),
357 })),
358 });
359 let keys: Vec<WireKey> = self.wires.keys().cloned().collect();
360 for key in keys {
361 if let Some(w) = self.wires.get_mut(&key) {
362 w.state = WireState::Pending;
363 }
364 self.queue(&key, pb::SubscriptionAction::Subscribe);
365 }
366 let reconnect = self.connections > 1;
367 let mut ids: Vec<SubscriptionId> = self.logical.keys().copied().collect();
368 ids.sort_by_key(|i| i.value());
369 for id in ids {
370 let l = self.logical.get_mut(&id).expect("listed");
371 l.state = LState::Pending;
372 if reconnect && matches!(l.kind, Kind::Book { .. } | Kind::Quotes) {
373 out.push_payload(
374 Some(id),
375 Event::Resync {
376 sub: id,
377 instrument: l.instrument.clone(),
378 reason: ResyncReason::SubscriptionReset,
379 },
380 );
381 }
382 }
383 }
384
385 fn find_wire(
386 &self,
387 kind: fn(&WireKey) -> bool,
388 uid: &str,
389 figi: &str,
390 ticker: &str,
391 class_code: &str,
392 ) -> Option<WireKey> {
393 self.wires
394 .iter()
395 .find(|(k, w)| kind(k) && matches(&w.instrument, uid, figi, ticker, class_code))
396 .map(|(k, _)| k.clone())
397 }
398
399 fn find_unsub(
400 &mut self,
401 kind: fn(&WireKey) -> bool,
402 uid: &str,
403 figi: &str,
404 ticker: &str,
405 class_code: &str,
406 ) -> bool {
407 let pos = self
408 .unsubscribing
409 .iter()
410 .position(|(k, i)| kind(k) && matches(i, uid, figi, ticker, class_code));
411 match pos {
412 Some(p) => {
413 self.unsubscribing.remove(p);
414 true
415 }
416 None => false,
417 }
418 }
419
420 #[allow(clippy::too_many_arguments)]
421 fn subscription_result(
422 &mut self,
423 kind: fn(&WireKey) -> bool,
424 action: i32,
425 status: i32,
426 ids: (&str, &str, &str, &str),
427 interval: Option<i32>,
428 now: Instant,
429 ) {
430 let (uid, figi, ticker, class_code) = ids;
431 let status =
432 pb::SubscriptionStatus::try_from(status).unwrap_or(pb::SubscriptionStatus::Unspecified);
433 if action == pb::SubscriptionAction::Unsubscribe as i32 {
434 if self.find_unsub(kind, uid, figi, ticker, class_code) {
435 if status == pb::SubscriptionStatus::Success {
436 self.unsub_ok += 1;
437 } else {
438 self.unsub_failed += 1;
439 }
440 }
441 return;
442 }
443 let key = self
444 .wires
445 .iter()
446 .find(|(k, w)| {
447 kind(k)
448 && matches(&w.instrument, uid, figi, ticker, class_code)
449 && match (k, interval) {
450 (WireKey::Candles(_, i), Some(x)) => *i == x,
451 _ => true,
452 }
453 })
454 .map(|(k, _)| k.clone());
455 let Some(key) = key else { return };
456 if status == pb::SubscriptionStatus::Success {
457 self.mark_active(&key);
458 } else {
459 self.mark_failed(&key, status, now);
460 }
461 }
462
463 fn on_response(&mut self, resp: pb::MarketDataResponse, now: Instant, out: &mut EventSink) {
464 let Some(payload) = resp.payload else { return };
465 match payload {
466 Resp::SubscribeOrderBookResponse(r) => {
467 for s in &r.order_book_subscriptions {
468 self.subscription_result(
469 |k| matches!(k, WireKey::Book(_)),
470 s.subscription_action,
471 s.subscription_status,
472 (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
473 None,
474 now,
475 );
476 }
477 }
478 Resp::SubscribeTradesResponse(r) => {
479 for s in &r.trade_subscriptions {
480 self.subscription_result(
481 |k| matches!(k, WireKey::Trades(_)),
482 s.subscription_action,
483 s.subscription_status,
484 (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
485 None,
486 now,
487 );
488 }
489 }
490 Resp::SubscribeCandlesResponse(r) => {
491 for s in &r.candles_subscriptions {
492 self.subscription_result(
493 |k| matches!(k, WireKey::Candles(..)),
494 s.subscription_action,
495 s.subscription_status,
496 (&s.instrument_uid, &s.figi, &s.ticker, &s.class_code),
497 Some(s.interval),
498 now,
499 );
500 }
501 }
502 Resp::Orderbook(b) => self.on_book(&b, now, out),
503 Resp::Trade(t) => self.on_trade(&t, now, out),
504 Resp::Candle(c) => self.on_candle(&c, now, out),
505 _ => {}
507 }
508 }
509
510 fn activate_on_data(&mut self, key: &WireKey) {
512 if matches!(
513 self.wires.get(key).map(|w| &w.state),
514 Some(WireState::Pending)
515 ) {
516 self.mark_active(key);
517 }
518 }
519
520 fn on_book(&mut self, b: &pb::OrderBook, now: Instant, out: &mut EventSink) {
521 let Some(key) = self.find_wire(
522 |k| matches!(k, WireKey::Book(_)),
523 &b.instrument_uid,
524 &b.figi,
525 &b.ticker,
526 &b.class_code,
527 ) else {
528 return;
529 };
530 self.activate_on_data(&key);
531 let mut ids: Vec<SubscriptionId> = self
532 .logical
533 .iter()
534 .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Book { .. } | Kind::Quotes))
535 .map(|(id, _)| *id)
536 .collect();
537 ids.sort_by_key(|i| i.value());
538 for id in ids {
539 let l = &self.logical[&id];
540 if !b.is_consistent {
541 out.push_payload(
543 Some(id),
544 Event::Resync {
545 sub: id,
546 instrument: l.instrument.clone(),
547 reason: ResyncReason::SequenceGap,
548 },
549 );
550 continue;
551 }
552 let payload = match l.kind {
553 Kind::Book { depth } => Event::OrderBook(market::book_snapshot(
554 &l.instrument,
555 &b.bids,
556 &b.asks,
557 depth as usize,
558 b.time.as_ref(),
559 now,
560 )),
561 Kind::Quotes => Event::Quote(market::quote(
562 &l.instrument,
563 &b.bids,
564 &b.asks,
565 b.time.as_ref(),
566 now,
567 )),
568 _ => continue,
569 };
570 out.push_payload(Some(id), payload);
571 }
572 }
573
574 fn on_trade(&mut self, t: &pb::Trade, now: Instant, out: &mut EventSink) {
575 let Some(key) = self.find_wire(
576 |k| matches!(k, WireKey::Trades(_)),
577 &t.instrument_uid,
578 &t.figi,
579 &t.ticker,
580 &t.class_code,
581 ) else {
582 return;
583 };
584 self.activate_on_data(&key);
585 let mut ids: Vec<SubscriptionId> = self
586 .logical
587 .iter()
588 .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Trades))
589 .map(|(id, _)| *id)
590 .collect();
591 ids.sort_by_key(|i| i.value());
592 for id in ids {
593 if let Some(m) = market::trade(&self.logical[&id].instrument, t, now) {
594 out.push_payload(Some(id), Event::MarketTrade(m));
595 }
596 }
597 }
598
599 fn on_candle(&mut self, c: &pb::Candle, now: Instant, out: &mut EventSink) {
600 let Some(key) = self.find_wire(
601 |k| matches!(k, WireKey::Candles(..)),
602 &c.instrument_uid,
603 &c.figi,
604 &c.ticker,
605 &c.class_code,
606 ) else {
607 return;
608 };
609 let Some(WireKey::Candles(r, _)) = Some(key) else {
611 return;
612 };
613 let key = WireKey::Candles(r, c.interval);
614 if !self.wires.contains_key(&key) {
615 return;
616 }
617 self.activate_on_data(&key);
618 let Some(tf) = market::timeframe_from_interval(c.interval()) else {
619 return;
620 };
621 let mut ids: Vec<SubscriptionId> = self
622 .logical
623 .iter()
624 .filter(|(_, l)| l.wire == key && matches!(l.kind, Kind::Candles(_)))
625 .map(|(id, _)| *id)
626 .collect();
627 ids.sort_by_key(|i| i.value());
628 for id in ids {
629 out.push_payload(
630 Some(id),
631 Event::Candle(market::stream_candle(
632 &self.logical[&id].instrument,
633 tf,
634 c,
635 now,
636 )),
637 );
638 }
639 }
640}
641
642impl BrokerCore for MdCore {
643 type Frame = MdFrame;
644
645 fn on_frame(&mut self, frame: MdFrame, now: Instant, out: &mut EventSink) {
646 match frame {
647 MdFrame::Connected => self.on_connected(out),
648 MdFrame::Response(r) => self.on_response(r, now, out),
649 }
650 }
651
652 fn poll_transmit(&mut self) -> Option<Outbound> {
654 None
655 }
656
657 fn poll_timeout(&self) -> Option<Instant> {
658 self.wires
659 .values()
660 .filter_map(|w| match w.state {
661 WireState::Failed { retry_at } => retry_at,
662 _ => None,
663 })
664 .min()
665 }
666
667 fn on_timeout(&mut self, now: Instant, _out: &mut EventSink) {
669 let due: Vec<WireKey> = self
670 .wires
671 .iter()
672 .filter(|(_, w)| matches!(w.state, WireState::Failed { retry_at: Some(t) } if t <= now))
673 .map(|(k, _)| k.clone())
674 .collect();
675 for key in due {
676 if let Some(w) = self.wires.get_mut(&key) {
677 w.state = WireState::Pending;
678 }
679 for l in self.logical.values_mut() {
680 if l.wire == key && l.state == LState::Failed {
681 l.state = LState::Pending;
682 }
683 }
684 self.queue(&key, pb::SubscriptionAction::Subscribe);
685 }
686 }
687
688 fn apply(&mut self, desired: &SubscriptionSet) {
689 let keep: Vec<SubscriptionId> = desired.iter().map(|d| d.id).collect();
690 self.logical.retain(|id, _| keep.contains(id));
691 for DesiredSubscription { id, kind } in desired.iter() {
692 if self.logical.contains_key(id) {
693 continue;
694 }
695 let (instrument, kind) = match kind {
696 SubscriptionKind::OrderBook { instrument, depth } => (
697 instrument.clone(),
698 Kind::Book {
699 depth: (*depth).max(1),
700 },
701 ),
702 SubscriptionKind::Quotes { instrument } => (instrument.clone(), Kind::Quotes),
703 SubscriptionKind::LastTrades { instrument } => (instrument.clone(), Kind::Trades),
704 SubscriptionKind::LastCandle {
705 instrument,
706 timeframe,
707 } => (instrument.clone(), Kind::Candles(*timeframe)),
708 SubscriptionKind::OrderState
710 | SubscriptionKind::Portfolio
711 | SubscriptionKind::Executions => continue,
712 };
713 let wire = wire_key(&instrument, &kind);
714 self.logical.insert(
715 *id,
716 Logical {
717 instrument,
718 kind,
719 wire,
720 state: LState::Pending,
721 },
722 );
723 }
724 self.desired = desired.clone();
725 self.sync_wires();
726 }
727}