1use std::collections::{BTreeMap, HashMap, VecDeque};
11use std::sync::Arc;
12use std::time::Instant;
13
14use brokapi_core::{
15 Balance, BrokerOrderId, ClientOrderId, Error, Event, InstrumentId, InstrumentKey, Money,
16 OrderSnapshot, OrderState, OrderType, PlaceOrderRequest, Price, Qty, Side, SubscriptionId,
17 Timestamps,
18};
19use brokapi_runtime::{FinishedWindow, OrderRuntime};
20use brokapi_sansio::{
21 BrokerCore, EventSink, OrderSignal, Outbound, SubscriptionKind, SubscriptionSet,
22};
23use brokapi_tbank_protocol::money::money_to_decimal;
24use brokapi_tbank_protocol::v1 as pb;
25use brokapi_tbank_protocol::v1::order_state_stream_response::Payload as OrderStreamPayload;
26use brokapi_tbank_protocol::v1::portfolio_stream_response::Payload as PortfolioPayload;
27use brokapi_tbank_protocol::v1::positions_stream_response::Payload as PositionsPayload;
28use brokapi_tbank_protocol::v1::trades_stream_response::Payload as TradesPayload;
29
30use crate::core::md::SubUpdate;
31use crate::ids::{id_from_parts, stamps};
32use crate::lots::LotBook;
33use crate::map::order::{self, OrderParts};
34use crate::map::{execution, portfolio};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum StreamKind {
39 OrderState,
41 Trades,
43 Portfolio,
45 Positions,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum UdSubKind {
53 OrderState,
55 Executions,
57 Portfolio,
59}
60
61#[derive(Debug, Clone)]
63pub enum UdFrame {
64 Connected(StreamKind),
66 OrderState(pb::OrderStateStreamResponse),
68 Trades(pb::TradesStreamResponse),
70 Portfolio(pb::PortfolioStreamResponse),
72 Positions(pb::PositionsStreamResponse),
74 ActiveOrders(pb::GetOrdersResponse),
76 StreamFailed(StreamKind, String),
79}
80
81#[derive(Debug, Clone)]
82struct OrderMeta {
83 instrument: InstrumentId,
84 side: Side,
85 order_type: OrderType,
86 qty: Qty,
87 filled: Qty,
88 price: Option<Price>,
89 exchange_id: Option<String>,
90}
91
92#[derive(Debug)]
123pub struct UdCore {
124 account_id: String,
125 orders: OrderRuntime,
126 meta: HashMap<ClientOrderId, OrderMeta>,
127 by_exchange: HashMap<String, ClientOrderId>,
128 pending_trades: HashMap<String, Vec<brokapi_core::Execution>>,
131 done: FinishedWindow<ClientOrderId>,
133 forgotten: Vec<ClientOrderId>,
135 known: HashMap<String, InstrumentKey>,
136 subs: HashMap<UdSubKind, SubscriptionId>,
137 cash: BTreeMap<String, (Money, Money)>,
138 portfolio_parts: (bool, bool),
139 updates: Vec<SubUpdate>,
140 connections: HashMap<StreamKind, u32>,
141 reconnected: VecDeque<StreamKind>,
142 lots: Arc<LotBook>,
143}
144
145impl UdCore {
146 pub fn new(account_id: impl Into<String>) -> Self {
148 Self {
149 account_id: account_id.into(),
150 orders: OrderRuntime::new(),
151 meta: HashMap::new(),
152 by_exchange: HashMap::new(),
153 pending_trades: HashMap::new(),
154 done: FinishedWindow::new(),
155 forgotten: Vec::new(),
156 known: HashMap::new(),
157 subs: HashMap::new(),
158 cash: BTreeMap::new(),
159 portfolio_parts: (false, false),
160 updates: Vec::new(),
161 connections: HashMap::new(),
162 reconnected: VecDeque::new(),
163 lots: Arc::new(LotBook::new()),
164 }
165 }
166
167 pub fn lots(&self) -> &Arc<LotBook> {
170 &self.lots
171 }
172
173 fn lot_of(&self, uid: &str, figi: &str, ticker: &str, class_code: &str) -> i64 {
176 match self.lots.get_parts(uid, figi, ticker, class_code) {
177 Some(lot) => lot,
178 None => {
179 tracing::warn!(
180 uid,
181 ticker,
182 "lot size of the instrument is not known, quantities stay in lots"
183 );
184 1
185 }
186 }
187 }
188
189 pub fn account_id(&self) -> &str {
191 &self.account_id
192 }
193
194 pub fn orders_mut(&mut self) -> &mut OrderRuntime {
196 &mut self.orders
197 }
198
199 pub fn order_state(&self, cid: ClientOrderId) -> Option<&OrderState> {
201 self.orders.state(cid)
202 }
203
204 pub fn take_forgotten(&mut self) -> Vec<ClientOrderId> {
207 std::mem::take(&mut self.forgotten)
208 }
209
210 pub fn take_updates(&mut self) -> Vec<SubUpdate> {
212 std::mem::take(&mut self.updates)
213 }
214
215 pub fn take_reconnected(&mut self) -> Vec<StreamKind> {
218 self.reconnected.drain(..).collect()
219 }
220
221 pub fn add_sub(&mut self, id: SubscriptionId, kind: UdSubKind) {
223 self.subs.insert(kind, id);
224 }
225
226 pub fn remove_sub(&mut self, kind: UdSubKind) {
228 self.subs.remove(&kind);
229 }
230
231 pub fn sub(&self, kind: UdSubKind) -> Option<SubscriptionId> {
233 self.subs.get(&kind).copied()
234 }
235
236 pub fn learn(&mut self, id: &InstrumentId) {
238 match &id.native {
239 brokapi_core::NativeId::Uid(u) | brokapi_core::NativeId::Figi(u) => {
240 self.known.insert(u.clone(), id.key.clone());
241 }
242 _ => {}
243 }
244 }
245
246 fn learn_parts(&mut self, uid: &str, figi: &str, ticker: &str, class_code: &str) {
247 if ticker.is_empty() {
248 return;
249 }
250 let key = InstrumentKey::new(ticker, class_code);
251 for k in [uid, figi] {
252 if !k.is_empty() {
253 self.known.insert(k.to_string(), key.clone());
254 }
255 }
256 }
257
258 pub fn resolve(&self, uid: &str, figi: &str, ticker: &str, class_code: &str) -> InstrumentId {
260 if ticker.is_empty() {
261 let key = self.known.get(uid).or_else(|| self.known.get(figi));
262 if let Some(key) = key {
263 return id_from_parts(uid, figi, &key.symbol, &key.mic);
264 }
265 }
266 id_from_parts(uid, figi, ticker, class_code)
267 }
268
269 fn other_account(&self, account_id: &str) -> bool {
270 !account_id.is_empty() && account_id != self.account_id
271 }
272
273 pub fn begin_place(&mut self, req: &PlaceOrderRequest, now: Instant) -> Result<(), Error> {
278 if !self.orders.try_place_once(req.cid) {
279 return Err(Error::Rejected {
280 code: "duplicate_place".into(),
281 message: "a cid is placed at most once; a repeated place is never sent".into(),
282 });
283 }
284 self.orders.register_place(req.cid, now);
285 self.learn(&req.instrument);
286 self.meta.insert(
287 req.cid,
288 OrderMeta {
289 instrument: req.instrument.clone(),
290 side: req.side,
291 order_type: req.order_type,
292 qty: req.qty,
293 filled: Qty::ZERO,
294 price: req.price,
295 exchange_id: None,
296 },
297 );
298 Ok(())
299 }
300
301 pub fn place_response(
303 &mut self,
304 cid: ClientOrderId,
305 resp: &pb::PostOrderResponse,
306 now: Instant,
307 out: &mut EventSink,
308 ) {
309 let lot = self.lot_of(
310 &resp.instrument_uid,
311 &resp.figi,
312 &resp.ticker,
313 &resp.class_code,
314 );
315 let parts = order::parts_from_post(resp).with_lot(lot);
316 self.learn_parts(
317 &resp.instrument_uid,
318 &resp.figi,
319 &resp.ticker,
320 &resp.class_code,
321 );
322 if !parts.exchange_id.is_empty() {
323 self.by_exchange.insert(parts.exchange_id.clone(), cid);
324 if let Some(m) = self.meta.get_mut(&cid) {
325 m.exchange_id = Some(parts.exchange_id.clone());
326 }
327 }
328 self.apply_parts(cid, &parts, now, out);
329 }
330
331 pub fn place_rejected(
333 &mut self,
334 cid: ClientOrderId,
335 reason: String,
336 now: Instant,
337 out: &mut EventSink,
338 ) {
339 self.signal(cid, OrderSignal::Rejected { reason }, now, out);
340 }
341
342 pub fn place_unknown(&mut self, cid: ClientOrderId, now: Instant, out: &mut EventSink) {
344 self.signal(cid, OrderSignal::Timeout, now, out);
345 }
346
347 pub fn cancel_ok(&mut self, cid: ClientOrderId, now: Instant, out: &mut EventSink) {
349 if self.orders.state(cid).is_none() {
350 return;
351 }
352 self.signal(cid, OrderSignal::Cancelled, now, out);
353 }
354
355 pub fn begin_replace(
358 &mut self,
359 new_cid: ClientOrderId,
360 original: ClientOrderId,
361 now: Instant,
362 ) -> Result<(), Error> {
363 if !self.orders.try_place_once(new_cid) {
364 return Err(Error::Rejected {
365 code: "duplicate_place".into(),
366 message: "a cid is used for a placement or replace at most once".into(),
367 });
368 }
369 self.orders.register_place(new_cid, now);
370 if let Some(m) = self.meta.get(&original).cloned() {
371 self.meta.insert(
372 new_cid,
373 OrderMeta {
374 exchange_id: None,
375 filled: Qty::ZERO,
376 ..m
377 },
378 );
379 }
380 Ok(())
381 }
382
383 pub fn replace_response(
385 &mut self,
386 new_cid: ClientOrderId,
387 resp: &pb::PostOrderResponse,
388 now: Instant,
389 out: &mut EventSink,
390 ) {
391 let instrument = self.resolve(
392 &resp.instrument_uid,
393 &resp.figi,
394 &resp.ticker,
395 &resp.class_code,
396 );
397 let lot = self.lot_of(
398 &resp.instrument_uid,
399 &resp.figi,
400 &resp.ticker,
401 &resp.class_code,
402 );
403 let parts = order::parts_from_post(resp).with_lot(lot);
404 let meta = self.meta.entry(new_cid).or_insert_with(|| OrderMeta {
405 instrument,
406 side: order::side(resp.direction()),
407 order_type: order::order_type(resp.order_type()),
408 qty: parts.qty_requested(),
409 filled: Qty::ZERO,
410 price: None,
411 exchange_id: None,
412 });
413 meta.qty = parts.qty_requested();
414 meta.filled = parts.qty_executed();
415 if let Some(p) = resp
416 .initial_security_price
417 .as_ref()
418 .map(|m| Price::new(money_to_decimal(m)))
419 .filter(|p| !p.is_zero())
420 {
421 meta.price = Some(p);
422 }
423 self.place_response(new_cid, resp, now, out);
424 }
425
426 pub fn replace_failed(
428 &mut self,
429 new_cid: ClientOrderId,
430 unknown: bool,
431 reason: String,
432 now: Instant,
433 out: &mut EventSink,
434 ) {
435 let signal = if unknown {
436 OrderSignal::Timeout
437 } else {
438 OrderSignal::Rejected { reason }
439 };
440 self.signal(new_cid, signal, now, out);
441 }
442
443 pub fn sweep_timeouts(&mut self, now: Instant, out: &mut EventSink) {
447 for (cid, _) in self.orders.sweep_timeouts(now) {
448 self.emit_order(cid, now, out);
449 }
450 self.settle_pending_trades(out);
451 }
452
453 pub fn set_command_timeout(&mut self, timeout: std::time::Duration) {
456 self.orders.set_command_timeout(timeout);
457 }
458
459 pub fn abandon_inflight(&mut self, now: Instant, out: &mut EventSink) -> Vec<ClientOrderId> {
461 let abandoned = self.orders.abandon_inflight(now);
462 for cid in &abandoned {
463 self.emit_order(*cid, now, out);
464 }
465 self.settle_pending_trades(out);
466 abandoned
467 }
468
469 pub fn exchange_id(&self, cid: ClientOrderId) -> Option<&str> {
471 self.meta.get(&cid).and_then(|m| m.exchange_id.as_deref())
472 }
473
474 pub fn instrument_of(&self, cid: ClientOrderId) -> Option<InstrumentId> {
476 self.meta.get(&cid).map(|m| m.instrument.clone())
477 }
478
479 pub fn order_basis(&self, cid: ClientOrderId) -> Option<(Qty, Option<Price>)> {
481 self.meta.get(&cid).map(|m| (m.qty, m.price))
482 }
483
484 pub fn note_order(&mut self, parts: &OrderParts) {
486 let cid = parts.cid();
487 self.learn_parts(
488 &instrument_uid(&parts.instrument),
489 "",
490 &parts.instrument.key.symbol,
491 &parts.instrument.key.mic,
492 );
493 if !parts.exchange_id.is_empty() {
494 self.by_exchange.insert(parts.exchange_id.clone(), cid);
495 }
496 let entry = self.meta.entry(cid).or_insert_with(|| OrderMeta {
497 instrument: parts.instrument.clone(),
498 side: order::side(parts.direction),
499 order_type: order::order_type(parts.order_type),
500 qty: parts.qty_requested(),
501 filled: parts.qty_executed(),
502 price: parts.unit_price,
503 exchange_id: None,
504 });
505 if !parts.exchange_id.is_empty() {
506 entry.exchange_id = Some(parts.exchange_id.clone());
507 }
508 }
509
510 fn release_trades(&mut self, order_id: &str, out: &mut EventSink) {
514 let Some(list) = self.pending_trades.remove(order_id) else {
515 return;
516 };
517 let cid = self.by_exchange.get(order_id).copied();
518 let sub = self.sub(UdSubKind::Executions);
519 for mut e in list {
520 e.cid = cid;
521 out.push_payload(sub, Event::Execution(e));
522 }
523 }
524
525 fn settle_pending_trades(&mut self, out: &mut EventSink) {
528 if self.pending_trades.is_empty() || self.orders.in_flight_count() > 0 {
529 return;
530 }
531 let ids: Vec<String> = self.pending_trades.keys().cloned().collect();
532 for id in ids {
533 self.release_trades(&id, out);
534 }
535 }
536
537 fn signal(
540 &mut self,
541 cid: ClientOrderId,
542 signal: OrderSignal,
543 now: Instant,
544 out: &mut EventSink,
545 ) {
546 let before = self.orders.state(cid).cloned();
547 if before.is_none() {
548 self.orders.register_place(cid, now);
549 }
550 self.orders.apply_signal(cid, signal, now);
551 if self.orders.state(cid) != before.as_ref() {
552 self.emit_order(cid, now, out);
553 }
554 self.settle_pending_trades(out);
555 }
556
557 fn emit_order(&mut self, cid: ClientOrderId, now: Instant, out: &mut EventSink) {
558 let (Some(state), Some(meta)) = (self.orders.state(cid).cloned(), self.meta.get(&cid))
559 else {
560 return;
561 };
562 let snap = OrderSnapshot::new(
563 cid,
564 meta.instrument.clone(),
565 state,
566 meta.side,
567 meta.order_type,
568 meta.qty,
569 Timestamps::new(crate::ids::wall_nanos(), now),
570 )
571 .with_broker_id(meta.exchange_id.clone().map(BrokerOrderId::new))
572 .with_filled(meta.filled)
573 .with_price(meta.price);
574 if snap.state.is_final() {
575 self.done.push(now, cid);
576 for old in self.done.expired(now) {
577 if let Some(m) = self.meta.remove(&old)
578 && let Some(id) = m.exchange_id
579 {
580 self.by_exchange.remove(&id);
581 }
582 self.forgotten.push(old);
583 }
584 }
585 out.push_payload(self.sub(UdSubKind::OrderState), Event::Order(snap));
586 }
587
588 fn apply_parts(
590 &mut self,
591 cid: ClientOrderId,
592 p: &OrderParts,
593 now: Instant,
594 out: &mut EventSink,
595 ) {
596 use pb::OrderExecutionReportStatus as S;
597 self.note_order(p);
598 if !p.exchange_id.is_empty() {
599 self.release_trades(&p.exchange_id, out);
600 }
601 if let Some(m) = self.meta.get_mut(&cid) {
602 m.filled = p.qty_executed();
603 if m.qty.is_zero() {
604 m.qty = p.qty_requested();
605 }
606 if m.price.is_none() {
608 m.price = p.unit_price;
609 }
610 }
611 let before = self.orders.state(cid).cloned();
612 if before.is_none() {
613 self.orders.register_place(cid, now);
614 }
615 let broker_id = BrokerOrderId::new(p.exchange_id.clone());
616 let ack = OrderSignal::Ack { broker_id };
617 let signals: Vec<OrderSignal> = match p.status {
618 S::ExecutionReportStatusNew => vec![ack],
619 S::ExecutionReportStatusPartiallyfill => vec![
620 ack,
621 OrderSignal::PartialFill {
622 filled: p.qty_executed(),
623 left: p.qty_left(),
624 },
625 ],
626 S::ExecutionReportStatusFill => vec![ack, OrderSignal::Filled],
627 S::ExecutionReportStatusCancelled => vec![ack, OrderSignal::Cancelled],
628 S::ExecutionReportStatusRejected => vec![OrderSignal::Rejected {
629 reason: if p.reason.is_empty() {
630 "REJECTED".into()
631 } else {
632 p.reason.clone()
633 },
634 }],
635 S::ExecutionReportStatusUnspecified => vec![],
636 };
637 for s in signals {
638 self.orders.apply_signal(cid, s, now);
640 }
641 if self.orders.state(cid) != before.as_ref() {
642 self.emit_order(cid, now, out);
643 }
644 self.settle_pending_trades(out);
645 }
646
647 fn on_order_state_stream(
650 &mut self,
651 r: pb::OrderStateStreamResponse,
652 now: Instant,
653 out: &mut EventSink,
654 ) {
655 match r.payload {
656 Some(OrderStreamPayload::OrderState(o)) => {
657 if self.other_account(&o.account_id) {
658 return;
659 }
660 self.learn_parts(&o.instrument_uid, "", &o.ticker, &o.class_code);
661 self.lots.put(
663 &o.instrument_uid,
664 "",
665 &o.ticker,
666 &o.class_code,
667 i64::from(o.lot_size),
668 );
669 let lot = self.lot_of(&o.instrument_uid, "", &o.ticker, &o.class_code);
670 let parts = order::parts_from_stream(&o).with_lot(lot);
671 let cid = parts.cid();
672 self.apply_parts(cid, &parts, now, out);
673 }
674 Some(OrderStreamPayload::Subscription(s)) => {
675 let ok = s.status() == pb::ResultSubscriptionStatus::Ok;
676 let reason = s
677 .error
678 .map(|e| format!("{}: {}", e.code, e.message))
679 .unwrap_or_else(|| "subscription refused".into());
680 self.resolve_sub(UdSubKind::OrderState, ok, reason);
681 }
682 _ => {}
683 }
684 }
685
686 fn on_trades_stream(&mut self, r: pb::TradesStreamResponse, now: Instant, out: &mut EventSink) {
687 match r.payload {
688 Some(TradesPayload::OrderTrades(t)) => {
689 if self.other_account(&t.account_id) {
690 return;
691 }
692 let instrument = self.resolve(&t.instrument_uid, &t.figi, "", "");
693 let cid = self.by_exchange.get(&t.order_id).copied();
694 let sub = self.sub(UdSubKind::Executions);
695 for e in execution::from_order_trades(&t, instrument, cid, now) {
696 if e.cid.is_none() && self.orders.in_flight_count() > 0 {
700 self.pending_trades
701 .entry(t.order_id.clone())
702 .or_default()
703 .push(e);
704 } else {
705 out.push_payload(sub, Event::Execution(e));
706 }
707 }
708 }
709 Some(TradesPayload::Subscription(s)) => {
710 let ok = s.status() == pb::ResultSubscriptionStatus::Ok;
711 let reason = s
712 .error
713 .map(|e| format!("{}: {}", e.code, e.message))
714 .unwrap_or_else(|| "subscription refused".into());
715 self.resolve_sub(UdSubKind::Executions, ok, reason);
716 }
717 _ => {}
718 }
719 }
720
721 fn cash_balances(&self, now: Instant) -> Vec<Balance> {
722 self.cash
723 .iter()
724 .map(|(currency, (available, blocked))| Balance {
725 currency: currency.clone(),
726 available: *available,
727 blocked: *blocked,
728 ts: stamps(now, None),
729 })
730 .collect()
731 }
732
733 fn set_cash(&mut self, balances: Vec<Balance>) {
734 for b in balances {
735 self.cash.insert(b.currency, (b.available, b.blocked));
736 }
737 }
738
739 fn on_portfolio_stream(
740 &mut self,
741 r: pb::PortfolioStreamResponse,
742 now: Instant,
743 out: &mut EventSink,
744 ) {
745 match r.payload {
746 Some(PortfolioPayload::Portfolio(p)) => {
747 if self.other_account(&p.account_id) {
748 return;
749 }
750 for pos in &p.positions {
751 self.learn_parts(&pos.instrument_uid, &pos.figi, &pos.ticker, &pos.class_code);
752 }
753 let snap = portfolio::portfolio(&p, self.cash_balances(now), now);
754 out.push_payload(self.sub(UdSubKind::Portfolio), Event::Portfolio(snap));
755 }
756 Some(PortfolioPayload::Subscriptions(s)) => {
757 let st = s
758 .accounts
759 .iter()
760 .find(|a| a.account_id == self.account_id)
761 .map(|a| a.subscription_status());
762 self.portfolio_parts.0 = st == Some(pb::PortfolioSubscriptionStatus::Success);
763 self.check_portfolio_ack(st.is_some(), "portfolio stream: account not subscribed");
764 }
765 _ => {}
766 }
767 }
768
769 fn on_positions_stream(
770 &mut self,
771 r: pb::PositionsStreamResponse,
772 now: Instant,
773 out: &mut EventSink,
774 ) {
775 let sub = self.sub(UdSubKind::Portfolio);
776 match r.payload {
777 Some(PositionsPayload::InitialPositions(p)) => {
778 if self.other_account(&p.account_id) {
779 return;
780 }
781 self.set_cash(portfolio::balances_from_positions(&p, now));
782 out.push_payload(sub, Event::Balances(self.cash_balances(now)));
783 }
784 Some(PositionsPayload::Position(p)) => {
785 if self.other_account(&p.account_id) {
786 return;
787 }
788 if !p.money.is_empty() {
789 self.set_cash(portfolio::balances_from_stream(&p.money, now));
790 out.push_payload(sub, Event::Balances(self.cash_balances(now)));
791 }
792 if !p.securities.is_empty() {
793 let resolved: Vec<InstrumentId> = p
794 .securities
795 .iter()
796 .map(|s| self.resolve(&s.instrument_uid, &s.figi, &s.ticker, &s.class_code))
797 .collect();
798 let positions =
799 portfolio::positions_from_securities(&p.securities, resolved, now);
800 out.push_payload(sub, Event::Positions(positions));
801 }
802 }
803 Some(PositionsPayload::Subscriptions(s)) => {
804 let st = s
805 .accounts
806 .iter()
807 .find(|a| a.account_id == self.account_id)
808 .map(|a| a.subscription_status());
809 self.portfolio_parts.1 = st
810 == Some(
811 pb::PositionsAccountSubscriptionStatus::PositionsSubscriptionStatusSuccess,
812 );
813 self.check_portfolio_ack(st.is_some(), "positions stream: account not subscribed");
814 }
815 _ => {}
816 }
817 }
818
819 fn check_portfolio_ack(&mut self, answered: bool, reason: &str) {
821 if !answered {
822 self.resolve_sub(UdSubKind::Portfolio, false, reason.to_string());
823 } else if self.portfolio_parts == (true, true) {
824 self.resolve_sub(UdSubKind::Portfolio, true, String::new());
825 }
826 }
827
828 fn resolve_sub(&mut self, kind: UdSubKind, ok: bool, reason: String) {
829 if let Some(id) = self.sub(kind) {
830 self.updates.push(SubUpdate {
831 sub: id,
832 result: if ok { Ok(()) } else { Err(reason) },
833 });
834 }
835 }
836
837 fn on_active_orders(&mut self, r: pb::GetOrdersResponse, now: Instant, out: &mut EventSink) {
838 for o in &r.orders {
839 self.learn_parts(&o.instrument_uid, &o.figi, &o.ticker, &o.class_code);
840 let lot = self.lot_of(&o.instrument_uid, &o.figi, &o.ticker, &o.class_code);
841 let parts = order::parts_from_state(o).with_lot(lot);
842 let cid = parts.cid();
843 self.apply_parts(cid, &parts, now, out);
844 }
845 }
846}
847
848fn instrument_uid(id: &InstrumentId) -> String {
849 match &id.native {
850 brokapi_core::NativeId::Uid(u) => u.clone(),
851 _ => String::new(),
852 }
853}
854
855impl BrokerCore for UdCore {
856 type Frame = UdFrame;
857
858 fn on_frame(&mut self, frame: UdFrame, now: Instant, out: &mut EventSink) {
859 match frame {
860 UdFrame::Connected(kind) => {
861 let n = self.connections.entry(kind).or_insert(0);
862 *n += 1;
863 if *n > 1 {
864 self.reconnected.push_back(kind);
865 let owner = match kind {
868 StreamKind::OrderState => Some(UdSubKind::OrderState),
869 StreamKind::Trades => Some(UdSubKind::Executions),
870 _ => None,
871 };
872 if let Some(sub) = owner.and_then(|k| self.sub(k)) {
873 out.push_payload(
874 Some(sub),
875 Event::Reconnected {
876 sub,
877 gap_from: crate::ids::wall_nanos(),
878 },
879 );
880 }
881 }
882 match kind {
883 StreamKind::Portfolio => self.portfolio_parts.0 = false,
884 StreamKind::Positions => self.portfolio_parts.1 = false,
885 _ => {}
886 }
887 }
888 UdFrame::OrderState(r) => self.on_order_state_stream(r, now, out),
889 UdFrame::Trades(r) => self.on_trades_stream(r, now, out),
890 UdFrame::Portfolio(r) => self.on_portfolio_stream(r, now, out),
891 UdFrame::Positions(r) => self.on_positions_stream(r, now, out),
892 UdFrame::ActiveOrders(r) => self.on_active_orders(r, now, out),
893 UdFrame::StreamFailed(kind, reason) => {
894 let sub = match kind {
895 StreamKind::OrderState => UdSubKind::OrderState,
896 StreamKind::Trades => UdSubKind::Executions,
897 StreamKind::Portfolio | StreamKind::Positions => UdSubKind::Portfolio,
898 };
899 self.resolve_sub(sub, false, reason);
900 }
901 }
902 }
903
904 fn poll_transmit(&mut self) -> Option<Outbound> {
906 None
907 }
908
909 fn poll_timeout(&self) -> Option<Instant> {
910 None
911 }
912
913 fn on_timeout(&mut self, _now: Instant, _out: &mut EventSink) {}
914
915 fn apply(&mut self, desired: &SubscriptionSet) {
917 self.subs.remove(&UdSubKind::OrderState);
918 self.subs.remove(&UdSubKind::Portfolio);
919 for d in desired.iter() {
920 match d.kind {
921 SubscriptionKind::OrderState => {
922 self.subs.insert(UdSubKind::OrderState, d.id);
923 }
924 SubscriptionKind::Portfolio => {
925 self.subs.insert(UdSubKind::Portfolio, d.id);
926 }
927 _ => {}
928 }
929 }
930 }
931}