1use std::collections::{HashMap, HashSet};
4use std::sync::{Arc, Mutex as StdMutex};
5use std::time::Instant;
6
7use async_trait::async_trait;
8use brokapi_alor_protocol::http::client::AlorHttpClient;
9use brokapi_alor_protocol::http::models::{
10 InstrumentRef, LimitOrderRequest, MarketOrderRequest, OrderCommandResponse, OrderRecord,
11 UserRef,
12};
13use brokapi_alor_protocol::ws::models::{WsIncoming, WsSubscribe};
14use brokapi_alor_protocol::ws::session::WsSession;
15use brokapi_core::{
16 Backpressure, Balance, ClientOrderId, ConfigError, Decimal, Envelope, Error, Execution,
17 ExecutionSource, InstrumentId, OrderAck, OrderExecutor, OrderSnapshot, OrderState,
18 OrderType as CoreOrderType, PlaceIdempotency, PlaceOrderRequest, PlaceStopOrderRequest,
19 Portfolio, PortfolioSource, Position, ReconcileReport, ReplaceOrderRequest, ShutdownMode,
20 ShutdownReport, Side, StopOrderExecutor, StopOrderRef, StopOrderSnapshot, SubscribeOpts,
21 SubscriptionId, TimeRange, UserDataCaps,
22};
23use brokapi_runtime::{
24 BrokerLimiter, CommandGate, Emitter, InstrumentLocks, OrderRuntime, PriceGrid,
25 ShutdownController,
26};
27use brokapi_sansio::OrderSignal;
28use tokio::sync::{Mutex, RwLock, mpsc};
29use tokio::task::JoinHandle;
30use tokio_util::sync::CancellationToken;
31
32use crate::cid::cid_from_comment;
33use crate::config::UserDataConfig;
34use crate::core::UdCore;
35use crate::core::ud::{BoardBook, board_key};
36use crate::error::{map_command_err, map_config_err, map_err};
37use crate::map::execution::execution;
38use crate::map::instrument::board_of;
39use crate::map::order::{order_ack, order_snapshot, state_of};
40use crate::map::portfolio::portfolio as map_portfolio;
41use crate::map::{parse_time, pieces_to_lots, stop as map_stop};
42use crate::pump::{drain_backlog, run_pump};
43use crate::raw::AlorRaw;
44use crate::subs::{SubIdGen, UdSubKind, UdSubscription, new_guid, wait_ack};
45use crate::util::{exchange_of_board, nanos_to_datetime, ticker_board};
46
47const TRADES_PAGE: usize = 1000;
49
50#[derive(Debug, Clone)]
52struct Tracked {
53 order_id: String,
54 stop: bool,
55}
56
57struct UdShared {
58 http: AlorHttpClient,
59 limiter: Arc<BrokerLimiter>,
60 core: Arc<Mutex<UdCore>>,
61 emitter: Arc<Emitter>,
62 orders: Mutex<OrderRuntime>,
63 instrument_locks: InstrumentLocks,
64 price_grid: PriceGrid,
65 subs: RwLock<HashMap<SubscriptionId, UdSubscription>>,
66 sub_ids: SubIdGen,
67 shutdown: Mutex<ShutdownController>,
68 cancel: CancellationToken,
69 order_state_buffer: usize,
70 portfolio: String,
71 exchange: String,
72 ack_timeout: std::time::Duration,
73 tracked: StdMutex<HashMap<ClientOrderId, Tracked>>,
76 lots: StdMutex<HashMap<(String, String), Decimal>>,
78 boards: BoardBook,
79}
80
81impl UdShared {
82 async fn prefetch(&self, msg: &WsIncoming) {
84 let WsIncoming::Data { guid, data } = msg else {
85 return;
86 };
87 let is_portfolio = self
88 .subs
89 .read()
90 .await
91 .values()
92 .any(|s| s.kind == UdSubKind::Portfolio && &s.guid == guid);
93 let symbol = data.get("symbol").and_then(|s| s.as_str());
94 let money = data.get("isCurrency").and_then(|b| b.as_bool()) == Some(true);
95 if let (true, false, Some(symbol)) = (is_portfolio, money, symbol) {
96 let _ = self.ensure_board(symbol).await;
97 }
98 }
99
100 async fn ensure_board(&self, symbol: &str) -> Result<String, Error> {
102 let key = board_key(&self.exchange, symbol);
103 if let Some(b) = self.boards.lock().ok().and_then(|b| b.get(&key).cloned()) {
104 return Ok(b);
105 }
106 self.limiter
107 .acquire("GetSecurity", brokapi_runtime::now())?;
108 let info = self
109 .http
110 .get_security(&self.exchange, symbol, None)
111 .await
112 .map_err(map_err)?;
113 let board = info
114 .primary_board
115 .clone()
116 .filter(|b| !b.is_empty())
117 .unwrap_or_else(|| board_of(&info));
118 if let Ok(mut b) = self.boards.lock() {
119 b.insert(key, board.clone());
120 }
121 if let Some(lot) = info.lot_size {
122 self.remember_lot(symbol, &board, lot);
123 }
124 Ok(board)
125 }
126
127 fn remember_lot(&self, ticker: &str, board: &str, lot: Decimal) {
128 if let Ok(mut l) = self.lots.lock() {
129 l.insert((ticker.to_string(), board.to_string()), lot);
130 }
131 }
132
133 fn track(&self, cid: ClientOrderId, order_id: &str, stop: bool) {
134 if !order_id.is_empty()
135 && let Ok(mut t) = self.tracked.lock()
136 {
137 t.insert(
138 cid,
139 Tracked {
140 order_id: order_id.to_string(),
141 stop,
142 },
143 );
144 }
145 }
146
147 fn tracked_of(&self, cid: ClientOrderId) -> Option<Tracked> {
148 self.tracked.lock().ok().and_then(|t| t.get(&cid).cloned())
149 }
150}
151
152pub struct UserDataConnector {
204 inner: Arc<UdShared>,
205 pump: JoinHandle<()>,
206 _ws: WsSession,
207}
208
209impl UserDataConnector {
210 pub async fn start(
219 cfg: UserDataConfig,
220 limiter: Arc<BrokerLimiter>,
221 out: mpsc::Sender<Envelope>,
222 ) -> Result<Self, Error> {
223 let http = AlorHttpClient::new(cfg.alor.clone()).map_err(map_config_err)?;
224 http.get_access_token().await.map_err(map_err)?;
225 let portfolio = match cfg.alor.portfolio.clone() {
226 Some(p) => p,
227 None => {
228 let mut named = http.token_portfolios().await.map_err(map_err)?;
229 if named.len() != 1 {
230 return Err(Error::Config(ConfigError::Message(format!(
231 "name the portfolio (ALOR_PORTFOLIO): the access token names {} of them",
232 named.len()
233 ))));
234 }
235 named.remove(0)
236 }
237 };
238
239 let (event_tx, event_rx) = kanal::bounded_async(512);
240 let ws = WsSession::spawn_with(http.clone(), event_tx, cfg.ws_options());
241 let cancel = CancellationToken::new();
242 let boards = BoardBook::default();
243 let core = Arc::new(Mutex::new(UdCore::new(
244 cfg.alor.exchange.clone(),
245 Arc::clone(&boards),
246 )));
247 let emitter = Emitter::new(Arc::clone(&cfg.source), out);
248 let gate = CommandGate::new();
249
250 let inner = Arc::new(UdShared {
251 http,
252 limiter,
253 core: Arc::clone(&core),
254 emitter: Arc::clone(&emitter),
255 orders: Mutex::new(OrderRuntime::new()),
256 instrument_locks: InstrumentLocks::new(),
257 price_grid: cfg.price_grid.clone(),
258 subs: RwLock::new(HashMap::new()),
259 sub_ids: SubIdGen::new(1000),
260 shutdown: Mutex::new(ShutdownController::new(gate)),
261 cancel: cancel.clone(),
262 order_state_buffer: cfg.order_state_buffer,
263 portfolio,
264 exchange: cfg.alor.exchange.clone(),
265 ack_timeout: cfg.subscribe_ack_timeout,
266 tracked: StdMutex::new(HashMap::new()),
267 lots: StdMutex::new(HashMap::new()),
268 boards,
269 });
270 let hook = Arc::clone(&inner);
271 let pump = tokio::spawn(run_pump(core, emitter, event_rx, cancel, move |m| {
272 let hook = Arc::clone(&hook);
273 async move {
274 hook.prefetch(&m).await;
275 m
276 }
277 }));
278 Ok(Self {
279 inner,
280 pump,
281 _ws: ws,
282 })
283 }
284
285 pub fn raw(&self) -> AlorRaw {
287 AlorRaw::new(self.inner.http.clone(), Arc::clone(&self.inner.limiter))
288 }
289
290 pub fn portfolio_id(&self) -> &str {
292 &self.inner.portfolio
293 }
294
295 pub fn price_grid(&self) -> &PriceGrid {
297 &self.inner.price_grid
298 }
299
300 pub fn caps(&self) -> UserDataCaps {
309 UserDataCaps::new(
310 PlaceIdempotency::LabelOnly,
311 false,
312 false,
313 true,
314 true,
315 true,
316 false,
317 )
318 }
319
320 pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport {
323 let now = brokapi_runtime::now();
324 if let ShutdownMode::Graceful { deadline } = mode {
325 drain_backlog(&self.inner.emitter, deadline).await;
326 }
327 let mut orders = self.inner.orders.lock().await;
328 let mut shutdown = self.inner.shutdown.lock().await;
329 let report = shutdown.shutdown_local(mode, now, &mut orders, || (0, 0));
330 self.inner.cancel.cancel();
331 let _ = self.pump.await;
332 report
333 }
334
335 async fn check_command(&self) -> Result<(), Error> {
336 let mut ctl = self.inner.shutdown.lock().await;
337 if self.inner.emitter.is_degraded() {
338 ctl.gate_mut().mark_degraded();
339 }
340 ctl.gate().check_command()
341 }
342
343 fn acquire(&self, method: &str) -> Result<(), Error> {
344 self.inner.limiter.acquire(method, brokapi_runtime::now())
345 }
346
347 fn exchange_for(&self, id: &InstrumentId) -> Result<(String, String), Error> {
349 let (ticker, board) = ticker_board(id)?;
350 let exchange = exchange_of_board(&board);
351 if exchange != self.inner.exchange {
352 return Err(Error::Config(ConfigError::Message(format!(
353 "{} trades on {exchange}, this connector works with {}: start another connector for it",
354 id.key.display(),
355 self.inner.exchange
356 ))));
357 }
358 Ok((ticker, board))
359 }
360
361 async fn lot_of(&self, ticker: &str, board: &str, id: &InstrumentId) -> Result<Decimal, Error> {
364 if let Some(lot) = self.inner.price_grid.lot(&id.key) {
365 return Ok(lot.decimal());
366 }
367 if let Some(lot) = self
368 .inner
369 .lots
370 .lock()
371 .ok()
372 .and_then(|l| l.get(&(ticker.to_string(), board.to_string())).copied())
373 {
374 return Ok(lot);
375 }
376 self.acquire("GetSecurity")?;
377 let info = self
378 .inner
379 .http
380 .get_security(&self.inner.exchange, ticker, Some(board))
381 .await
382 .map_err(map_err)?;
383 let lot = info
384 .lot_size
385 .filter(|l| !l.is_zero())
386 .unwrap_or(Decimal::ONE);
387 self.inner.remember_lot(ticker, board, lot);
388 Ok(lot)
389 }
390
391 async fn register_ud_sub(
392 &self,
393 kind: UdSubKind,
394 opts: SubscribeOpts,
395 ws: impl FnOnce(&str, &str, &str) -> WsSubscribe,
396 ) -> Result<SubscriptionId, Error> {
397 let id = self.inner.sub_ids.next();
398 let guid = new_guid();
399 let instrument = crate::util::instrument_id("", "");
400 self.inner
401 .emitter
402 .register(id, Some(instrument), opts.backpressure);
403 self.inner.core.lock().await.register(&guid, id, kind);
404 let request = ws(&guid, &self.inner.exchange, &self.inner.portfolio);
405 let notify = self.inner.core.lock().await.notifier();
406 if let Err(e) = self._ws.subscribe(request).await {
407 self.inner.core.lock().await.forget(&guid);
408 self.inner.emitter.unregister(id);
409 return Err(map_err(e));
410 }
411 let confirmed = wait_ack(
413 &self.inner.core,
414 notify,
415 self.inner.ack_timeout,
416 |c: &UdCore| {
417 (
418 c.actual_state(id).unwrap_or(crate::core::SubState::Pending),
419 c.failure(id).map(str::to_string),
420 )
421 },
422 )
423 .await;
424 if let Err(e) = confirmed {
425 let _ = self._ws.unsubscribe(&guid).await;
426 self.inner.core.lock().await.forget(&guid);
427 self.inner.emitter.unregister(id);
428 return Err(e);
429 }
430 self.inner.subs.write().await.insert(
431 id,
432 UdSubscription {
433 id,
434 kind,
435 opts,
436 guid,
437 },
438 );
439 Ok(id)
440 }
441
442 async fn list_orders(&self) -> Result<Vec<OrderRecord>, Error> {
443 self.acquire("GetOrders")?;
444 self.inner
445 .http
446 .get_orders(&self.inner.exchange, &self.inner.portfolio)
447 .await
448 .map_err(map_err)
449 }
450
451 async fn list_stop_orders(
452 &self,
453 ) -> Result<Vec<brokapi_alor_protocol::http::models::StopOrderRecord>, Error> {
454 self.acquire("GetStopOrders")?;
455 self.inner
456 .http
457 .get_stop_orders(&self.inner.exchange, &self.inner.portfolio)
458 .await
459 .map_err(map_err)
460 }
461
462 async fn find_order(&self, cid: ClientOrderId, stops: bool) -> Result<Tracked, Error> {
465 if let Some(t) = self.inner.tracked_of(cid) {
466 return Ok(t);
467 }
468 for r in self.list_orders().await? {
469 if cid_from_comment(r.comment.as_deref()) == Some(cid)
470 || crate::cid::foreign_cid(&r.id) == cid
471 {
472 self.inner.track(cid, &r.id, false);
473 return Ok(Tracked {
474 order_id: r.id,
475 stop: false,
476 });
477 }
478 }
479 if stops {
480 for r in self.list_stop_orders().await? {
481 if map_stop::cid_of(&r) == Some(cid) {
482 self.inner.track(cid, &r.id, true);
483 return Ok(Tracked {
484 order_id: r.id,
485 stop: true,
486 });
487 }
488 }
489 }
490 Err(Error::not_found(
491 "the broker has no order with this client order id",
492 ))
493 }
494
495 async fn settle(&self, cid: ClientOrderId, outcome: &Result<OrderAck, Error>) {
496 let signal = match outcome {
497 Ok(ack) => OrderSignal::Ack {
498 broker_id: ack.broker_id.clone(),
499 },
500 Err(Error::Rejected { message, .. }) => OrderSignal::Rejected {
501 reason: message.clone(),
502 },
503 Err(_) => OrderSignal::Timeout,
504 };
505 self.inner
506 .orders
507 .lock()
508 .await
509 .apply_signal(cid, signal, brokapi_runtime::now());
510 }
511
512 fn ack_from(
515 &self,
516 cid: ClientOrderId,
517 resp: &OrderCommandResponse,
518 stop: bool,
519 ) -> Result<OrderAck, Error> {
520 let Some(number) = resp.order_number.as_deref().filter(|n| !n.is_empty()) else {
521 return Err(Error::Unknown);
522 };
523 self.inner.track(cid, number, stop);
524 Ok(order_ack(cid, number, brokapi_runtime::now()))
525 }
526
527 async fn begin_place(&self, cid: ClientOrderId, what: &str) -> Result<(), Error> {
529 let now = brokapi_runtime::now();
530 let mut orders = self.inner.orders.lock().await;
531 if !orders.try_place_once(cid) {
532 return Err(Error::Rejected {
533 code: "duplicate_place".into(),
534 message: format!("auto-retry of {what} is forbidden"),
535 });
536 }
537 orders.register_place(cid, now);
538 Ok(())
539 }
540
541 async fn snapshot_of(
542 &self,
543 cid: ClientOrderId,
544 record: &OrderRecord,
545 ) -> Result<OrderSnapshot, Error> {
546 order_snapshot(record, cid, brokapi_runtime::now()).ok_or_else(|| {
547 Error::Transport(brokapi_core::TransportError::Message(
548 "the order answer has no quantity in pieces".into(),
549 ))
550 })
551 }
552}
553
554fn side_word(side: Side) -> String {
555 match side {
556 Side::Buy => "buy",
557 Side::Sell => "sell",
558 }
559 .to_string()
560}
561
562#[async_trait]
563impl brokapi_core::Connector for UserDataConnector {
564 type Caps = brokapi_core::UserDataCaps;
565
566 fn caps(&self) -> brokapi_core::UserDataCaps {
567 UserDataConnector::caps(self)
568 }
569
570 async fn shutdown(self, mode: brokapi_core::ShutdownMode) -> brokapi_core::ShutdownReport {
571 UserDataConnector::shutdown(self, mode).await
572 }
573}
574
575#[async_trait]
576impl OrderExecutor for UserDataConnector {
577 async fn place(&self, req: PlaceOrderRequest) -> Result<OrderAck, Error> {
578 self.check_command().await?;
579 let (ticker, board) = self.exchange_for(&req.instrument)?;
582 self.inner
583 .price_grid
584 .check_qty(&req.instrument.key, req.qty)?;
585 let price = match req.order_type {
586 CoreOrderType::Market => None,
587 CoreOrderType::Limit => {
588 let price = req.price.ok_or_else(|| Error::Rejected {
589 code: "missing_price".into(),
590 message: "limit order requires price".into(),
591 })?;
592 self.inner.price_grid.check(&req.instrument.key, price)?;
594 Some(price)
595 }
596 };
597 let lot = self.lot_of(&ticker, &board, &req.instrument).await?;
598 let lots = pieces_to_lots(req.qty, lot)?;
599 self.acquire("PlaceOrder")?;
600
601 let _instrument = self.inner.instrument_locks.lock(&req.instrument).await;
603 self.check_command().await?;
606 self.inner.instrument_locks.bind(req.cid, &req.instrument);
607 self.begin_place(req.cid, "place").await?;
608
609 let comment = Some(req.cid.hyphenated().to_string());
610 let instrument = InstrumentRef {
611 symbol: ticker,
612 exchange: self.inner.exchange.clone(),
613 instrument_group: Some(board),
614 };
615 let user = UserRef {
616 portfolio: self.inner.portfolio.clone(),
617 };
618 let req_id = req.cid.hyphenated().to_string();
619 let result = match price {
620 Some(price) => {
621 let body = LimitOrderRequest {
622 side: side_word(req.side),
623 quantity: lots,
624 price: price.decimal(),
625 instrument,
626 user,
627 comment,
628 time_in_force: Some("oneday".into()),
629 allow_margin: None,
630 iceberg_fixed: None,
631 iceberg_variance: None,
632 };
633 self.inner.http.create_limit_order(&req_id, &body).await
634 }
635 None => {
636 let body = MarketOrderRequest {
637 side: side_word(req.side),
638 quantity: lots,
639 instrument,
640 user,
641 comment,
642 time_in_force: Some("oneday".into()),
643 allow_margin: None,
644 };
645 self.inner.http.create_market_order(&req_id, &body).await
646 }
647 };
648 let outcome = match result {
649 Ok(resp) => self.ack_from(req.cid, &resp, false),
650 Err(e) => Err(map_command_err(e)),
651 };
652 self.settle(req.cid, &outcome).await;
653 let ack = outcome?;
654 self.inner
655 .core
656 .lock()
657 .await
658 .learn_order_id(ack.broker_id.as_str(), req.cid);
659 Ok(ack)
660 }
661
662 async fn cancel(&self, cid: ClientOrderId) -> Result<(), Error> {
663 self.check_command().await?;
664 self.acquire("CancelOrder")?;
665 let _instrument = self.inner.instrument_locks.lock_cid(cid).await;
667 self.check_command().await?;
669 let t = self.find_order(cid, true).await.map_err(|e| match e {
670 e if e.is_not_found() => Error::Rejected {
671 code: brokapi_core::NOT_FOUND.into(),
672 message: "the broker has no order with this client order id".into(),
673 },
674 e => e,
675 })?;
676 self.inner
677 .http
678 .cancel_order(
679 &self.inner.exchange,
680 &self.inner.portfolio,
681 &t.order_id,
682 t.stop,
683 )
684 .await
685 .map_err(map_command_err)?;
686 Ok(())
687 }
688
689 async fn replace(&self, req: ReplaceOrderRequest) -> Result<OrderAck, Error> {
690 self.check_command().await?;
691 let t = self.find_order(req.original_cid, false).await?;
693 self.acquire("GetOrder")?;
694 let cur = self
695 .inner
696 .http
697 .get_order(&self.inner.exchange, &self.inner.portfolio, &t.order_id)
698 .await
699 .map_err(map_err)?;
700 if cur.kind.as_deref() == Some("market") {
701 return Err(Error::Rejected {
702 code: "not_replaceable".into(),
703 message: "a market order cannot be replaced".into(),
704 });
705 }
706 let snap = self.snapshot_of(req.original_cid, &cur).await?;
707 if req.qty.is_none() && snap.filled.is_positive() {
708 return Err(Error::Rejected {
709 code: "qty_required".into(),
710 message: "the order is partly filled: give the quantity of the new order".into(),
711 });
712 }
713 let (ticker, board) = self.exchange_for(&snap.instrument)?;
714 let price = req.price.or(snap.price).ok_or_else(|| Error::Rejected {
715 code: "missing_price".into(),
716 message: "the order has no price to keep".into(),
717 })?;
718 self.inner.price_grid.check(&snap.instrument.key, price)?;
719 let qty = req.qty.unwrap_or(snap.qty);
720 self.inner.price_grid.check_qty(&snap.instrument.key, qty)?;
721 let lot = self.lot_of(&ticker, &board, &snap.instrument).await?;
722 let lots = pieces_to_lots(qty, lot)?;
723
724 self.acquire("ReplaceOrder")?;
725 let _instrument = self.inner.instrument_locks.lock_cid(req.original_cid).await;
726 self.check_command().await?;
728 self.inner.instrument_locks.bind(req.cid, &snap.instrument);
729 let body = LimitOrderRequest {
730 side: side_word(snap.side),
731 quantity: lots,
732 price: price.decimal(),
733 instrument: InstrumentRef {
734 symbol: ticker,
735 exchange: self.inner.exchange.clone(),
736 instrument_group: Some(board),
737 },
738 user: UserRef {
739 portfolio: self.inner.portfolio.clone(),
740 },
741 comment: Some(req.cid.hyphenated().to_string()),
742 time_in_force: cur
743 .time_in_force
744 .clone()
745 .or_else(|| Some("oneday".to_string())),
746 allow_margin: None,
747 iceberg_fixed: None,
748 iceberg_variance: None,
749 };
750 let resp = self
751 .inner
752 .http
753 .update_limit_order(&req.cid.hyphenated().to_string(), &t.order_id, &body)
754 .await
755 .map_err(map_command_err)?;
756 let ack = self.ack_from(req.cid, &resp, false)?;
757 self.inner
758 .core
759 .lock()
760 .await
761 .learn_order_id(ack.broker_id.as_str(), req.cid);
762 Ok(ack)
763 }
764
765 async fn order_state(&self, cid: ClientOrderId) -> Result<OrderSnapshot, Error> {
766 let t = self.find_order(cid, false).await?;
767 self.acquire("GetOrder")?;
768 let rec = self
769 .inner
770 .http
771 .get_order(&self.inner.exchange, &self.inner.portfolio, &t.order_id)
772 .await
773 .map_err(map_err)?;
774 self.snapshot_of(cid, &rec).await
775 }
776
777 async fn active_orders(&self) -> Result<Vec<OrderSnapshot>, Error> {
778 let records = self.list_orders().await?;
779 let now = brokapi_runtime::now();
780 let mut out = Vec::new();
781 let mut core = self.inner.core.lock().await;
782 for r in &records {
783 if !is_working(r.status.as_deref()) {
784 continue;
785 }
786 let cid = core.resolve_cid(&r.id, r.comment.as_deref());
787 self.inner.track(cid, &r.id, false);
790 if let Some(snap) = order_snapshot(r, cid, now) {
791 out.push(snap);
792 }
793 }
794 Ok(out)
795 }
796
797 async fn subscribe_order_state(&self) -> Result<SubscriptionId, Error> {
798 let opts = lossless(self.inner.order_state_buffer);
799 self.register_ud_sub(UdSubKind::OrderState, opts, |guid, ex, pf| {
800 WsSubscribe::orders(guid, ex, pf, true)
801 })
802 .await
803 }
804
805 async fn reconcile(&self, ledger: &[ClientOrderId]) -> Result<ReconcileReport, Error> {
806 self.acquire("Reconcile")?;
807 let orders = self.list_orders().await?;
808 let stops = self.list_stop_orders().await?;
809 let now = brokapi_runtime::now();
810 let mut report = ReconcileReport::new(Vec::new(), Vec::new(), Vec::new());
811 let known: HashSet<ClientOrderId> = ledger.iter().copied().collect();
812 for cid in ledger {
813 let found_order = orders.iter().find(|r| {
814 cid_from_comment(r.comment.as_deref()) == Some(*cid)
815 || self
816 .inner
817 .tracked_of(*cid)
818 .is_some_and(|t| !t.stop && t.order_id == r.id)
819 });
820 if let Some(r) = found_order {
821 self.inner.track(*cid, &r.id, false);
822 let snap = self.snapshot_of(*cid, r).await?;
823 match snap.state {
824 OrderState::Filled { .. }
825 | OrderState::Cancelled { .. }
826 | OrderState::Rejected { .. } => {
827 report.closed_offline.push(*cid);
828 }
829 _ => report.confirmed.push(snap),
831 }
832 continue;
833 }
834 let found_stop = stops.iter().find(|r| {
835 map_stop::cid_of(r) == Some(*cid)
836 || self
837 .inner
838 .tracked_of(*cid)
839 .is_some_and(|t| t.stop && t.order_id == r.id)
840 });
841 match found_stop {
842 Some(r)
843 if matches!(
844 map_stop::state_of(r.status.as_deref()),
845 brokapi_core::StopOrderState::Active
846 ) =>
847 {
848 self.inner.track(*cid, &r.id, true);
849 if let Some(s) = stop_as_order(r, *cid, now) {
850 report.confirmed.push(s);
851 } else {
852 return Err(Error::Transport(brokapi_core::TransportError::Message(
854 "a stop order of the ledger cannot be read".into(),
855 )));
856 }
857 }
858 _ => report.closed_offline.push(*cid),
860 }
861 }
862 report.foreign = self
863 .active_orders()
864 .await?
865 .into_iter()
866 .filter(|o| !o.cid.is_nil() && !known.contains(&o.cid))
867 .collect();
868 self.inner.emitter.clear_degraded();
871 let mut ctl = self.inner.shutdown.lock().await;
872 ctl.gate_mut().clear_degraded();
873 ctl.gate_mut().mark_reconciled();
874 Ok(report)
875 }
876}
877
878fn stop_as_order(
880 r: &brokapi_alor_protocol::http::models::StopOrderRecord,
881 cid: ClientOrderId,
882 now: Instant,
883) -> Option<OrderSnapshot> {
884 let s = crate::map::stop::snapshot(r, Some(cid), now)?;
885 Some(
886 OrderSnapshot::new(
887 cid,
888 s.instrument,
889 OrderState::Acked {
890 cid,
891 broker_id: s.broker_id.clone(),
892 },
893 s.side,
894 if s.limit_price.is_some() {
895 CoreOrderType::Limit
896 } else {
897 CoreOrderType::Market
898 },
899 s.qty,
900 s.ts,
901 )
902 .with_broker_id(Some(s.broker_id))
903 .with_price(s.limit_price),
904 )
905}
906
907fn is_working(status: Option<&str>) -> bool {
908 !matches!(
909 state_of(
910 status,
911 uuid::Uuid::nil(),
912 "",
913 brokapi_core::Qty::ZERO,
914 brokapi_core::Qty::ZERO
915 ),
916 OrderState::Filled { .. } | OrderState::Cancelled { .. } | OrderState::Rejected { .. }
917 )
918}
919
920fn lossless(cap: usize) -> SubscribeOpts {
921 SubscribeOpts {
922 backpressure: Backpressure::Lossless { cap },
923 depth: None,
924 }
925}
926
927#[async_trait]
928impl ExecutionSource for UserDataConnector {
929 async fn executions(&self, range: TimeRange) -> Result<Vec<Execution>, Error> {
930 let now = brokapi_runtime::now();
931 let mut seen = HashSet::new();
932 let mut out = Vec::new();
933 self.acquire("GetTrades")?;
937 let from = nanos_to_datetime(range.from).format("%Y-%m-%d").to_string();
938 let query = [
939 ("dateFrom", from),
940 ("limit", TRADES_PAGE.to_string()),
941 ("orderByTradeDate", "true".to_string()),
942 ];
943 let history = self
944 .inner
945 .http
946 .get_history_trades(&self.inner.exchange, &self.inner.portfolio, &query)
947 .await
948 .map_err(map_err)?;
949 if history.len() >= TRADES_PAGE {
950 return Err(Error::Rejected {
951 code: "range_too_long".into(),
952 message: format!(
953 "the history holds {TRADES_PAGE} trades at most per answer and the range reaches that: read it in shorter ranges"
954 ),
955 });
956 }
957 self.acquire("GetTrades")?;
959 let today = self
960 .inner
961 .http
962 .get_trades(&self.inner.exchange, &self.inner.portfolio)
963 .await
964 .map_err(map_err)?;
965 let core = self.inner.core.lock().await;
966 for r in history.iter().chain(today.iter()) {
967 if !seen.insert(r.id.clone()) {
968 continue;
969 }
970 let at = parse_time(r.date.as_deref());
971 if at.is_some_and(|t| t < range.from || t >= range.to) {
972 continue;
973 }
974 let order_no = r.orderno.clone().unwrap_or_default();
975 let cid =
976 cid_from_comment(r.comment.as_deref()).or_else(|| core.cid_of_order_id(&order_no));
977 if let Some(e) = execution(r, cid, now) {
978 out.push(e);
979 }
980 }
981 Ok(out)
982 }
983
984 async fn subscribe_executions(&self) -> Result<SubscriptionId, Error> {
985 let opts = lossless(self.inner.order_state_buffer);
986 self.register_ud_sub(UdSubKind::Executions, opts, |guid, ex, pf| {
987 WsSubscribe::trades(guid, ex, pf, true)
988 })
989 .await
990 }
991}
992
993#[async_trait]
994impl PortfolioSource for UserDataConnector {
995 async fn portfolio(&self) -> Result<Portfolio, Error> {
996 self.acquire("GetPositions")?;
997 let records = self
998 .inner
999 .http
1000 .get_positions(&self.inner.exchange, &self.inner.portfolio, false)
1001 .await
1002 .map_err(map_err)?;
1003 for r in &records {
1005 if r.is_currency != Some(true) && r.qty_units.is_some_and(|q| !q.is_zero()) {
1006 self.inner.ensure_board(&r.symbol).await?;
1007 }
1008 }
1009 let boards = Arc::clone(&self.inner.boards);
1010 let exchange = self.inner.exchange.clone();
1011 Ok(map_portfolio(
1012 &records,
1013 |symbol| {
1014 boards
1015 .lock()
1016 .ok()
1017 .and_then(|b| b.get(&board_key(&exchange, symbol)).cloned())
1018 .unwrap_or_default()
1019 },
1020 brokapi_runtime::now(),
1021 ))
1022 }
1023
1024 async fn positions(&self) -> Result<Vec<Position>, Error> {
1025 Ok(self.portfolio().await?.positions)
1026 }
1027
1028 async fn balances(&self) -> Result<Vec<Balance>, Error> {
1029 Ok(self.portfolio().await?.balances)
1030 }
1031
1032 async fn subscribe_portfolio(&self, opts: SubscribeOpts) -> Result<SubscriptionId, Error> {
1033 self.register_ud_sub(UdSubKind::Portfolio, opts, |guid, ex, pf| {
1035 WsSubscribe::positions(guid, ex, pf, false)
1036 })
1037 .await
1038 }
1039}
1040
1041#[async_trait]
1042impl StopOrderExecutor for UserDataConnector {
1043 async fn place_stop(&self, req: PlaceStopOrderRequest) -> Result<OrderAck, Error> {
1044 self.check_command().await?;
1045 let (ticker, board) = self.exchange_for(&req.instrument)?;
1047 self.inner
1048 .price_grid
1049 .check_qty(&req.instrument.key, req.qty)?;
1050 self.inner
1051 .price_grid
1052 .check(&req.instrument.key, req.stop_price)?;
1053 if let Some(p) = req.limit_price {
1054 self.inner.price_grid.check(&req.instrument.key, p)?;
1055 }
1056 let lot = self.lot_of(&ticker, &board, &req.instrument).await?;
1057 let now_secs = chrono::Utc::now().timestamp();
1058 let (wire, limit) = map_stop::build(
1059 &req,
1060 lot,
1061 &self.inner.portfolio,
1062 &self.inner.exchange,
1063 now_secs,
1064 req.cid,
1065 )?;
1066 self.acquire("PlaceStopOrder")?;
1068 let _instrument = self.inner.instrument_locks.lock(&req.instrument).await;
1069 self.check_command().await?;
1071 self.inner.instrument_locks.bind(req.cid, &req.instrument);
1072 self.begin_place(req.cid, "place_stop").await?;
1073 let req_id = req.cid.hyphenated().to_string();
1074 let result = if limit {
1075 self.inner
1076 .http
1077 .create_stop_limit_order(&req_id, &wire)
1078 .await
1079 } else {
1080 self.inner.http.create_stop_order(&req_id, &wire).await
1081 };
1082 let outcome = match result {
1083 Ok(resp) => self.ack_from(req.cid, &resp, true),
1084 Err(e) => Err(map_command_err(e)),
1085 };
1086 self.settle(req.cid, &outcome).await;
1087 let ack = outcome?;
1088 self.inner
1089 .core
1090 .lock()
1091 .await
1092 .learn_order_id(ack.broker_id.as_str(), req.cid);
1093 Ok(ack)
1094 }
1095
1096 async fn cancel_stop(&self, stop: StopOrderRef) -> Result<(), Error> {
1097 self.check_command().await?;
1098 self.acquire("CancelStopOrder")?;
1099 let (known_cid, order_id) = match stop {
1100 StopOrderRef::Cid(cid) => {
1101 let t = self.find_order(cid, true).await.map_err(|e| {
1102 if e.is_not_found() {
1103 Error::Rejected {
1104 code: brokapi_core::NOT_FOUND.into(),
1105 message: "the broker has no stop order with this client order id"
1106 .into(),
1107 }
1108 } else {
1109 e
1110 }
1111 })?;
1112 (cid, t.order_id)
1113 }
1114 StopOrderRef::Broker(id) => {
1115 let cid = self
1116 .inner
1117 .core
1118 .lock()
1119 .await
1120 .cid_of_order_id(id.as_str())
1121 .unwrap_or_else(|| crate::cid::foreign_cid(id.as_str()));
1122 (cid, id.as_str().to_string())
1123 }
1124 };
1125 let _instrument = self.inner.instrument_locks.lock_cid(known_cid).await;
1126 self.check_command().await?;
1128 self.inner
1129 .http
1130 .cancel_order(&self.inner.exchange, &self.inner.portfolio, &order_id, true)
1131 .await
1132 .map_err(map_command_err)?;
1133 Ok(())
1134 }
1135
1136 async fn active_stop_orders(&self) -> Result<Vec<StopOrderSnapshot>, Error> {
1137 let records = self.list_stop_orders().await?;
1138 let now = brokapi_runtime::now();
1139 let core = self.inner.core.lock().await;
1140 let mut out = Vec::new();
1141 for r in &records {
1142 let cid = map_stop::cid_of(r).or_else(|| core.cid_of_order_id(&r.id));
1143 if let Some(cid) = cid {
1144 self.inner.track(cid, &r.id, true);
1145 }
1146 if let Some(s) = map_stop::snapshot(r, cid, now)
1147 && matches!(s.state, brokapi_core::StopOrderState::Active)
1148 {
1149 out.push(s);
1150 }
1151 }
1152 Ok(out)
1153 }
1154
1155 async fn subscribe_stop_orders(&self) -> Result<SubscriptionId, Error> {
1156 Err(Error::Unsupported {
1158 capability: "stop_order_stream".into(),
1159 })
1160 }
1161}