1use std::collections::{HashMap, HashSet};
4use std::sync::{Arc, Mutex as StdMutex};
5use std::time::Instant;
6
7use async_trait::async_trait;
8use brokapi_bybit_protocol::http::client::BybitHttpClient;
9use brokapi_bybit_protocol::http::models::{
10 CancelOrderRequest, CreateOrderRequest, InstrumentInfo, OrderRecord,
11};
12use brokapi_bybit_protocol::ws::session::{WsSession, WsStream};
13use brokapi_core::{
14 Backpressure, Balance, ClientOrderId, Envelope, Error, Event, Execution, ExecutionSource,
15 InstrumentId, OrderAck, OrderExecutor, OrderSnapshot, OrderState, OrderType as CoreOrderType,
16 PlaceIdempotency, PlaceOrderRequest, PlaceStopOrderRequest, Portfolio, PortfolioSource,
17 Position, Price, Qty, ReconcileReport, ReplaceOrderRequest, ShutdownMode, ShutdownReport,
18 StopOrderExecutor, StopOrderRef, StopOrderSnapshot, SubscribeOpts, SubscriptionId, TimeRange,
19 UserDataCaps,
20};
21use brokapi_runtime::{
22 BrokerLimiter, CommandGate, Emitter, InstrumentLocks, OrderRuntime, PriceGrid,
23 ShutdownController,
24};
25use brokapi_sansio::{CoreEvent, OrderSignal};
26use tokio::sync::{Mutex, RwLock, mpsc};
27use tokio::task::JoinHandle;
28use tokio_util::sync::CancellationToken;
29
30use crate::cid::cid_from_link_id;
31use crate::config::UserDataConfig;
32use crate::core::UdCore;
33use crate::core::ud::{WALLET_TOPIC, execution_topic, order_topic, position_topic};
34use crate::error::{map_command_err, map_config_err, map_err};
35use crate::map::execution::execution;
36use crate::map::order::{is_final, is_untriggered, order_ack, order_snapshot};
37use crate::map::portfolio::portfolio as map_portfolio;
38use crate::map::{ms_to_nanos, side_word, stop as map_stop};
39use crate::pump::{drain_backlog, run_pump};
40use crate::raw::BybitRaw;
41use crate::subs::{ReqIdGen, SubIdGen, UdSubKind, UdSubscription, wait_ack};
42use crate::util::{nanos_to_ms, symbol_category};
43
44const ORDERS_PAGE: u32 = 50;
46const EXECUTIONS_PAGE: u32 = 100;
48const POSITIONS_PAGE: u32 = 200;
50const MAX_PAGES: u32 = 40;
52const EXECUTION_WINDOW_MS: i64 = 7 * 24 * 3600 * 1000;
54const EXECUTION_HISTORY_MS: i64 = 2 * 365 * 24 * 3600 * 1000;
56
57#[derive(Debug, Clone)]
59struct Tracked {
60 order_id: String,
61 symbol: String,
62}
63
64#[derive(Debug, Clone)]
66struct Rules {
67 qty_step: Qty,
68 min_qty: Qty,
69 tick: Price,
70}
71
72impl Rules {
73 fn of(info: &InstrumentInfo) -> Self {
74 Self {
75 qty_step: Qty::new(info.lot_size_filter.qty_step),
76 min_qty: Qty::new(info.lot_size_filter.min_order_qty),
77 tick: Price::new(info.price_filter.tick_size),
78 }
79 }
80
81 fn check_qty(&self, qty: Qty) -> Result<(), Error> {
82 let bad = |why: String| Error::Rejected {
83 code: "invalid_qty".into(),
84 message: why,
85 };
86 if !qty.is_positive() {
87 return Err(bad(format!("quantity must be positive, got {qty}")));
88 }
89 if self.qty_step.is_positive() && !qty.is_on_step(self.qty_step) {
90 return Err(bad(format!(
91 "quantity {qty} is not a whole number of steps of {}",
92 self.qty_step
93 )));
94 }
95 if qty < self.min_qty {
96 return Err(bad(format!(
97 "quantity {qty} is below the minimum {}",
98 self.min_qty
99 )));
100 }
101 Ok(())
102 }
103
104 fn check_price(&self, price: Price) -> Result<(), Error> {
105 brokapi_core::check_price_step(price, self.tick)
106 }
107}
108
109struct UdShared {
110 http: BybitHttpClient,
111 ws: WsSession,
112 limiter: Arc<BrokerLimiter>,
113 core: Arc<Mutex<UdCore>>,
114 emitter: Arc<Emitter>,
115 orders: Mutex<OrderRuntime>,
116 instrument_locks: InstrumentLocks,
117 price_grid: PriceGrid,
118 subs: RwLock<HashMap<SubscriptionId, UdSubscription>>,
119 sub_ids: SubIdGen,
120 req_ids: ReqIdGen,
121 shutdown: Mutex<ShutdownController>,
122 cancel: CancellationToken,
123 order_state_buffer: usize,
124 category: String,
125 settle_coin: String,
126 demo: bool,
127 ack_timeout: std::time::Duration,
128 tracked: StdMutex<HashMap<ClientOrderId, Tracked>>,
130 rules: StdMutex<HashMap<String, Rules>>,
132}
133
134impl UdShared {
135 fn track(&self, cid: ClientOrderId, order_id: &str, symbol: &str) {
136 if !order_id.is_empty()
137 && let Ok(mut t) = self.tracked.lock()
138 {
139 t.insert(
140 cid,
141 Tracked {
142 order_id: order_id.to_string(),
143 symbol: symbol.to_string(),
144 },
145 );
146 }
147 }
148
149 fn tracked_of(&self, cid: ClientOrderId) -> Option<Tracked> {
150 self.tracked.lock().ok().and_then(|t| t.get(&cid).cloned())
151 }
152}
153
154pub struct UserDataConnector {
207 inner: Arc<UdShared>,
208 pump: JoinHandle<()>,
209}
210
211impl UserDataConnector {
212 pub async fn start(
222 cfg: UserDataConfig,
223 limiter: Arc<BrokerLimiter>,
224 out: mpsc::Sender<Envelope>,
225 ) -> Result<Self, Error> {
226 let http = BybitHttpClient::new(cfg.bybit.clone()).map_err(map_config_err)?;
227 http.sync_time().await.map_err(map_err)?;
228 http.get_wallet_balance().await.map_err(map_err)?;
229
230 let (event_tx, event_rx) = kanal::bounded_async(512);
231 let ws = WsSession::spawn(http.clone(), WsStream::Private, event_tx, cfg.ws_options());
232 let cancel = CancellationToken::new();
233 let category = cfg.bybit.category.as_str().to_string();
234 let core = Arc::new(Mutex::new(UdCore::new(category.clone())));
235 let emitter = Emitter::new(Arc::clone(&cfg.source), out);
236 let gate = CommandGate::new();
237
238 let inner = Arc::new(UdShared {
239 http,
240 ws,
241 limiter,
242 core: Arc::clone(&core),
243 emitter: Arc::clone(&emitter),
244 orders: Mutex::new(OrderRuntime::new()),
245 instrument_locks: InstrumentLocks::new(),
246 price_grid: cfg.price_grid.clone(),
247 subs: RwLock::new(HashMap::new()),
248 sub_ids: SubIdGen::new(1000),
249 req_ids: ReqIdGen::default(),
250 shutdown: Mutex::new(ShutdownController::new(gate)),
251 cancel: cancel.clone(),
252 order_state_buffer: cfg.order_state_buffer,
253 category,
254 settle_coin: cfg.bybit.settle_coin.clone(),
255 demo: cfg.bybit.is_demo(),
256 ack_timeout: cfg.subscribe_ack_timeout,
257 tracked: StdMutex::new(HashMap::new()),
258 rules: StdMutex::new(HashMap::new()),
259 });
260 let pump = tokio::spawn(run_pump(core, emitter, event_rx, cancel, || async {}));
261 Ok(Self { inner, pump })
262 }
263
264 pub fn raw(&self) -> BybitRaw {
266 BybitRaw::new(self.inner.http.clone(), Arc::clone(&self.inner.limiter))
267 }
268
269 pub fn price_grid(&self) -> &PriceGrid {
272 &self.inner.price_grid
273 }
274
275 pub fn caps(&self) -> UserDataCaps {
287 UserDataCaps::new(
288 PlaceIdempotency::LabelOnly,
289 false,
290 false,
291 false,
292 true,
293 true,
294 self.inner.demo,
295 )
296 }
297
298 pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport {
301 let now = brokapi_runtime::now();
302 if let ShutdownMode::Graceful { deadline } = mode {
303 drain_backlog(&self.inner.emitter, deadline).await;
304 }
305 let mut orders = self.inner.orders.lock().await;
306 let mut shutdown = self.inner.shutdown.lock().await;
307 let report = shutdown.shutdown_local(mode, now, &mut orders, || (0, 0));
308 self.inner.cancel.cancel();
309 let _ = self.pump.await;
310 report
311 }
312
313 async fn check_command(&self) -> Result<(), Error> {
314 let mut ctl = self.inner.shutdown.lock().await;
315 if self.inner.emitter.is_degraded() {
316 ctl.gate_mut().mark_degraded();
317 }
318 ctl.gate().check_command()
319 }
320
321 fn acquire(&self, method: &str) -> Result<(), Error> {
322 self.inner.limiter.acquire(method, brokapi_runtime::now())
323 }
324
325 fn symbol(&self, id: &InstrumentId) -> Result<String, Error> {
326 symbol_category(id, &self.inner.category)
327 }
328
329 async fn rules_of(&self, symbol: &str) -> Result<Rules, Error> {
331 if let Some(r) = self
332 .inner
333 .rules
334 .lock()
335 .ok()
336 .and_then(|m| m.get(symbol).cloned())
337 {
338 return Ok(r);
339 }
340 self.acquire("GetInstruments")?;
341 let info = self
342 .inner
343 .http
344 .get_instrument(&self.inner.category, symbol)
345 .await
346 .map_err(map_err)?;
347 let rules = Rules::of(&info);
348 if let Ok(mut m) = self.inner.rules.lock() {
349 m.insert(symbol.to_string(), rules.clone());
350 }
351 Ok(rules)
352 }
353
354 async fn all_open_orders(&self) -> Result<Vec<OrderRecord>, Error> {
356 let mut all = Vec::new();
357 let mut cursor: Option<String> = None;
358 for page in 0..MAX_PAGES {
359 if page > 0 {
360 self.acquire("GetOrders")?;
361 }
362 let batch = self
363 .inner
364 .http
365 .get_open_orders(
366 &self.inner.category,
367 None,
368 Some(&self.inner.settle_coin),
369 None,
370 None,
371 ORDERS_PAGE,
372 cursor.as_deref(),
373 )
374 .await
375 .map_err(map_err)?;
376 all.extend(batch.list);
377 if batch.next_page_cursor.is_empty() {
378 return Ok(all);
379 }
380 cursor = Some(batch.next_page_cursor);
381 }
382 Err(Error::Transport(brokapi_core::TransportError::Message(
383 format!(
384 "the list of open orders is longer than {MAX_PAGES} pages: not returned in part"
385 ),
386 )))
387 }
388
389 async fn find_record(&self, cid: ClientOrderId) -> Result<Option<OrderRecord>, Error> {
392 let tracked = self.inner.tracked_of(cid);
393 let (order_id, link_id) = match &tracked {
394 Some(t) => (Some(t.order_id.clone()), None),
395 None => (None, Some(cid.hyphenated().to_string())),
396 };
397 self.acquire("GetOrder")?;
398 let open = self
399 .inner
400 .http
401 .get_open_orders(
402 &self.inner.category,
403 None,
404 Some(&self.inner.settle_coin),
405 order_id.as_deref(),
406 link_id.as_deref(),
407 ORDERS_PAGE,
408 None,
409 )
410 .await
411 .map_err(map_err)?;
412 if let Some(r) = open.list.into_iter().next() {
413 return Ok(Some(r));
414 }
415 self.acquire("GetOrderHistory")?;
416 let history = self
417 .inner
418 .http
419 .get_order_history(
420 &self.inner.category,
421 None,
422 order_id.as_deref(),
423 link_id.as_deref(),
424 ORDERS_PAGE,
425 None,
426 )
427 .await
428 .map_err(map_err)?;
429 if let Some(r) = history.list.into_iter().next() {
430 return Ok(Some(r));
431 }
432 if tracked.is_some() {
433 return Ok(None);
434 }
435 let working = self.all_open_orders().await?;
437 Ok(working
438 .into_iter()
439 .find(|r| crate::cid::foreign_cid(&r.order_id) == cid))
440 }
441
442 async fn find_order(&self, cid: ClientOrderId) -> Result<Tracked, Error> {
444 if let Some(t) = self.inner.tracked_of(cid) {
445 return Ok(t);
446 }
447 match self.find_record(cid).await? {
448 Some(r) => {
449 self.inner.track(cid, &r.order_id, &r.symbol);
450 Ok(Tracked {
451 order_id: r.order_id,
452 symbol: r.symbol,
453 })
454 }
455 None => Err(Error::not_found(
456 "the exchange has no order with this client order id",
457 )),
458 }
459 }
460
461 async fn settle(&self, cid: ClientOrderId, outcome: &Result<OrderAck, Error>) {
462 let signal = match outcome {
463 Ok(ack) => OrderSignal::Ack {
464 broker_id: ack.broker_id.clone(),
465 },
466 Err(Error::Rejected { message, .. }) => OrderSignal::Rejected {
467 reason: message.clone(),
468 },
469 Err(_) => OrderSignal::Timeout,
470 };
471 self.inner
472 .orders
473 .lock()
474 .await
475 .apply_signal(cid, signal, brokapi_runtime::now());
476 }
477
478 fn ack_from(
481 &self,
482 cid: ClientOrderId,
483 order_id: &str,
484 symbol: &str,
485 ) -> Result<OrderAck, Error> {
486 if order_id.is_empty() {
487 return Err(Error::Unknown);
488 }
489 self.inner.track(cid, order_id, symbol);
490 Ok(order_ack(cid, order_id, brokapi_runtime::now()))
491 }
492
493 async fn begin_place(&self, cid: ClientOrderId, what: &str) -> Result<(), Error> {
495 let now = brokapi_runtime::now();
496 let mut orders = self.inner.orders.lock().await;
497 if !orders.try_place_once(cid) {
498 return Err(Error::Rejected {
499 code: "duplicate_place".into(),
500 message: format!("auto-retry of {what} is forbidden"),
501 });
502 }
503 orders.register_place(cid, now);
504 Ok(())
505 }
506
507 fn snapshot_of(
508 &self,
509 cid: ClientOrderId,
510 record: &OrderRecord,
511 ) -> Result<OrderSnapshot, Error> {
512 order_snapshot(record, cid, &self.inner.category, brokapi_runtime::now()).ok_or_else(|| {
513 Error::Transport(brokapi_core::TransportError::Message(
514 "the order answer has a side the connector cannot read".into(),
515 ))
516 })
517 }
518
519 async fn send_create(
521 &self,
522 cid: ClientOrderId,
523 symbol: &str,
524 body: CreateOrderRequest,
525 ) -> Result<OrderAck, Error> {
526 let result = self.inner.http.create_order(&body).await;
527 let outcome = match result {
528 Ok(r) => self.ack_from(cid, &r.order_id, symbol),
529 Err(e) => Err(map_command_err(e)),
530 };
531 self.settle(cid, &outcome).await;
532 let ack = outcome?;
533 self.inner
534 .core
535 .lock()
536 .await
537 .learn_order_id(ack.broker_id.as_str(), cid);
538 Ok(ack)
539 }
540
541 async fn register_ud_sub(
542 &self,
543 kind: UdSubKind,
544 opts: SubscribeOpts,
545 topics: Vec<String>,
546 ) -> Result<SubscriptionId, Error> {
547 let id = self.inner.sub_ids.next();
548 self.inner.emitter.register(id, None, opts.backpressure);
549 let reqs: Vec<(String, String)> = topics
550 .iter()
551 .map(|t| (self.inner.req_ids.next(), t.clone()))
552 .collect();
553 let (fresh, notify) = {
555 let mut core = self.inner.core.lock().await;
556 let fresh: Vec<(String, String)> = reqs
557 .iter()
558 .filter(|(_, t)| !core.has_topic(t))
559 .cloned()
560 .collect();
561 core.register(id, kind, &topics, &reqs);
562 (fresh, core.notifier())
563 };
564 for (req, topic) in &fresh {
565 if let Err(e) = self.inner.ws.subscribe(req, topic).await {
566 self.drop_sub(id).await;
567 return Err(map_err(e));
568 }
569 }
570 let confirmed = wait_ack(
572 &self.inner.core,
573 notify,
574 self.inner.ack_timeout,
575 |c: &UdCore| c.sub_state(id),
576 )
577 .await;
578 if let Err(e) = confirmed {
579 self.drop_sub(id).await;
580 return Err(e);
581 }
582 self.inner.subs.write().await.insert(
583 id,
584 UdSubscription {
585 id,
586 kind,
587 opts,
588 topics,
589 },
590 );
591 Ok(id)
592 }
593
594 async fn drop_sub(&self, id: SubscriptionId) {
595 let orphans = self.inner.core.lock().await.forget(id);
596 self.inner.emitter.unregister(id);
597 for topic in orphans {
598 let _ = self.inner.ws.unsubscribe(&topic).await;
599 }
600 }
601
602 async fn read_portfolio(&self) -> Result<Portfolio, Error> {
604 self.acquire("GetWallet")?;
605 let wallets = self
606 .inner
607 .http
608 .get_wallet_balance()
609 .await
610 .map_err(map_err)?;
611 let mut records = Vec::new();
612 let mut cursor: Option<String> = None;
613 for page in 0..MAX_PAGES {
614 self.acquire("GetPositions")?;
615 let batch = self
616 .inner
617 .http
618 .get_positions(
619 &self.inner.category,
620 &self.inner.settle_coin,
621 POSITIONS_PAGE,
622 cursor.as_deref(),
623 )
624 .await
625 .map_err(map_err)?;
626 records.extend(batch.list);
627 if batch.next_page_cursor.is_empty() {
628 break;
629 }
630 cursor = Some(batch.next_page_cursor);
631 if page + 1 == MAX_PAGES {
632 return Err(Error::Transport(brokapi_core::TransportError::Message(
633 "the list of positions is too long: not returned in part".into(),
634 )));
635 }
636 }
637 Ok(map_portfolio(
638 &wallets,
639 &records,
640 &self.inner.category,
641 brokapi_runtime::now(),
642 ))
643 }
644}
645
646#[async_trait]
647impl brokapi_core::Connector for UserDataConnector {
648 type Caps = brokapi_core::UserDataCaps;
649
650 fn caps(&self) -> brokapi_core::UserDataCaps {
651 UserDataConnector::caps(self)
652 }
653
654 async fn shutdown(self, mode: brokapi_core::ShutdownMode) -> brokapi_core::ShutdownReport {
655 UserDataConnector::shutdown(self, mode).await
656 }
657}
658
659#[async_trait]
660impl OrderExecutor for UserDataConnector {
661 async fn place(&self, req: PlaceOrderRequest) -> Result<OrderAck, Error> {
662 self.check_command().await?;
663 let symbol = self.symbol(&req.instrument)?;
666 self.inner
667 .price_grid
668 .check_qty(&req.instrument.key, req.qty)?;
669 let price = match req.order_type {
670 CoreOrderType::Market => None,
671 CoreOrderType::Limit => {
672 let price = req.price.ok_or_else(|| Error::Rejected {
673 code: "missing_price".into(),
674 message: "limit order requires price".into(),
675 })?;
676 self.inner.price_grid.check(&req.instrument.key, price)?;
678 Some(price)
679 }
680 };
681 let rules = self.rules_of(&symbol).await?;
682 rules.check_qty(req.qty)?;
683 if let Some(p) = price {
684 rules.check_price(p)?;
685 }
686 self.acquire("PlaceOrder")?;
687
688 let _instrument = self.inner.instrument_locks.lock(&req.instrument).await;
690 self.check_command().await?;
693 self.inner.instrument_locks.bind(req.cid, &req.instrument);
694 self.begin_place(req.cid, "place").await?;
695
696 let body = CreateOrderRequest {
697 category: self.inner.category.clone(),
698 symbol: symbol.clone(),
699 side: side_word(req.side).to_string(),
700 order_type: if price.is_some() { "Limit" } else { "Market" }.to_string(),
701 qty: req.qty.decimal(),
702 price: price.map(Price::decimal),
703 time_in_force: price.map(|_| "GTC".to_string()),
705 order_link_id: Some(req.cid.hyphenated().to_string()),
706 ..Default::default()
707 };
708 self.send_create(req.cid, &symbol, body).await
709 }
710
711 async fn cancel(&self, cid: ClientOrderId) -> Result<(), Error> {
712 self.check_command().await?;
713 self.acquire("CancelOrder")?;
714 let _instrument = self.inner.instrument_locks.lock_cid(cid).await;
716 self.check_command().await?;
718 let t = self.find_order(cid).await.map_err(|e| match e {
719 e if e.is_not_found() => Error::Rejected {
720 code: brokapi_core::NOT_FOUND.into(),
721 message: "the exchange has no order with this client order id".into(),
722 },
723 e => e,
724 })?;
725 self.inner
726 .http
727 .cancel_order(&CancelOrderRequest {
728 category: self.inner.category.clone(),
729 symbol: t.symbol,
730 order_id: Some(t.order_id),
731 order_link_id: None,
732 })
733 .await
734 .map_err(map_command_err)?;
735 Ok(())
736 }
737
738 async fn replace(&self, _req: ReplaceOrderRequest) -> Result<OrderAck, Error> {
739 Err(Error::Unsupported {
742 capability: "replace".into(),
743 })
744 }
745
746 async fn order_state(&self, cid: ClientOrderId) -> Result<OrderSnapshot, Error> {
747 match self.find_record(cid).await? {
748 Some(rec) => {
749 self.inner.track(cid, &rec.order_id, &rec.symbol);
750 self.snapshot_of(cid, &rec)
751 }
752 None => Err(Error::not_found(
753 "the exchange has no order with this client order id",
754 )),
755 }
756 }
757
758 async fn active_orders(&self) -> Result<Vec<OrderSnapshot>, Error> {
759 let records = self.all_open_orders().await?;
760 let now = brokapi_runtime::now();
761 let mut out = Vec::new();
762 let mut core = self.inner.core.lock().await;
763 for r in &records {
764 if is_untriggered(r) || is_final(r) {
765 continue;
766 }
767 let cid = core.resolve_cid(&r.order_id, &r.order_link_id);
768 self.inner.track(cid, &r.order_id, &r.symbol);
771 if let Some(snap) = order_snapshot(r, cid, &self.inner.category, now) {
772 out.push(snap);
773 }
774 }
775 Ok(out)
776 }
777
778 async fn subscribe_order_state(&self) -> Result<SubscriptionId, Error> {
779 let opts = lossless(self.inner.order_state_buffer);
780 self.register_ud_sub(
781 UdSubKind::OrderState,
782 opts,
783 vec![order_topic(&self.inner.category)],
784 )
785 .await
786 }
787
788 async fn reconcile(&self, ledger: &[ClientOrderId]) -> Result<ReconcileReport, Error> {
789 self.acquire("Reconcile")?;
790 let open = self.all_open_orders().await?;
791 let now = brokapi_runtime::now();
792 let mut report = ReconcileReport::new(Vec::new(), Vec::new(), Vec::new());
793 let known: HashSet<ClientOrderId> = ledger.iter().copied().collect();
794 for cid in ledger {
795 let tracked = self.inner.tracked_of(*cid);
796 let working = open.iter().find(|r| {
797 cid_from_link_id(Some(&r.order_link_id)) == Some(*cid)
798 || tracked.as_ref().is_some_and(|t| t.order_id == r.order_id)
799 });
800 if let Some(r) = working {
801 self.inner.track(*cid, &r.order_id, &r.symbol);
802 let snap = if is_untriggered(r) {
803 stop_as_order(r, *cid, &self.inner.category, now)
804 } else {
805 order_snapshot(r, *cid, &self.inner.category, now)
806 };
807 match snap {
808 Some(s) => report.confirmed.push(s),
809 None => {
811 return Err(Error::Transport(brokapi_core::TransportError::Message(
812 "an order of the ledger cannot be read".into(),
813 )));
814 }
815 }
816 continue;
817 }
818 match self.find_record(*cid).await? {
820 Some(r) if !is_final(&r) => {
821 self.inner.track(*cid, &r.order_id, &r.symbol);
824 if let Some(s) = order_snapshot(&r, *cid, &self.inner.category, now) {
825 report.confirmed.push(s);
826 }
827 }
828 _ => report.closed_offline.push(*cid),
829 }
830 }
831 {
832 let mut core = self.inner.core.lock().await;
833 report.foreign = open
834 .iter()
835 .filter(|r| !is_untriggered(r) && !is_final(r))
836 .filter_map(|r| {
837 let cid = core.resolve_cid(&r.order_id, &r.order_link_id);
838 (!known.contains(&cid))
839 .then(|| order_snapshot(r, cid, &self.inner.category, now))?
840 })
841 .collect();
842 }
843 self.inner.emitter.clear_degraded();
846 let mut ctl = self.inner.shutdown.lock().await;
847 ctl.gate_mut().clear_degraded();
848 ctl.gate_mut().mark_reconciled();
849 Ok(report)
850 }
851}
852
853fn stop_as_order(
855 r: &OrderRecord,
856 cid: ClientOrderId,
857 category: &str,
858 now: Instant,
859) -> Option<OrderSnapshot> {
860 let s = map_stop::snapshot(r, Some(cid), category, now)?;
861 Some(
862 OrderSnapshot::new(
863 cid,
864 s.instrument,
865 OrderState::Acked {
866 cid,
867 broker_id: s.broker_id.clone(),
868 },
869 s.side,
870 if s.limit_price.is_some() {
871 CoreOrderType::Limit
872 } else {
873 CoreOrderType::Market
874 },
875 s.qty,
876 s.ts,
877 )
878 .with_broker_id(Some(s.broker_id))
879 .with_price(s.limit_price),
880 )
881}
882
883fn lossless(cap: usize) -> SubscribeOpts {
884 SubscribeOpts {
885 backpressure: Backpressure::Lossless { cap },
886 depth: None,
887 }
888}
889
890#[async_trait]
891impl ExecutionSource for UserDataConnector {
892 async fn executions(&self, range: TimeRange) -> Result<Vec<Execution>, Error> {
893 let now_ms = chrono::Utc::now().timestamp_millis();
894 let end = nanos_to_ms(range.to).min(now_ms);
895 let start = nanos_to_ms(range.from).max(end - EXECUTION_HISTORY_MS);
898 let mut seen = HashSet::new();
899 let mut out = Vec::new();
900 let mut from = start;
901 while from <= end {
902 let to = (from + EXECUTION_WINDOW_MS).min(end);
903 let mut cursor: Option<String> = None;
904 for page in 0..MAX_PAGES {
905 self.acquire("GetExecutions")?;
906 let batch = self
907 .inner
908 .http
909 .get_executions(
910 &self.inner.category,
911 None,
912 Some(from),
913 Some(to),
914 EXECUTIONS_PAGE,
915 cursor.as_deref(),
916 )
917 .await
918 .map_err(map_err)?;
919 let nanos = brokapi_runtime::now();
920 let core = self.inner.core.lock().await;
921 for r in &batch.list {
922 if !seen.insert(r.exec_id.clone()) {
923 continue;
924 }
925 let at = ms_to_nanos(r.exec_time);
926 if r.exec_time > 0 && (at < range.from || at >= range.to) {
927 continue;
928 }
929 let cid = cid_from_link_id(Some(&r.order_link_id))
930 .or_else(|| core.cid_of_order_id(&r.order_id));
931 if let Some(e) = execution(r, cid, &self.inner.category, nanos) {
932 out.push(e);
933 }
934 }
935 drop(core);
936 if batch.next_page_cursor.is_empty() {
937 break;
938 }
939 cursor = Some(batch.next_page_cursor);
940 if page + 1 == MAX_PAGES {
941 return Err(Error::Rejected {
942 code: "range_too_long".into(),
943 message: "a window of 7 days holds more fills than the connector reads: read a shorter range".into(),
944 });
945 }
946 }
947 if to >= end {
948 break;
949 }
950 from = to + 1;
951 }
952 out.sort_by_key(|e| e.ts.broker_ts.map(|t| t.as_nanos()).unwrap_or(0));
953 Ok(out)
954 }
955
956 async fn subscribe_executions(&self) -> Result<SubscriptionId, Error> {
957 let opts = lossless(self.inner.order_state_buffer);
958 self.register_ud_sub(
959 UdSubKind::Executions,
960 opts,
961 vec![execution_topic(&self.inner.category)],
962 )
963 .await
964 }
965}
966
967#[async_trait]
968impl PortfolioSource for UserDataConnector {
969 async fn portfolio(&self) -> Result<Portfolio, Error> {
970 self.read_portfolio().await
971 }
972
973 async fn positions(&self) -> Result<Vec<Position>, Error> {
974 Ok(self.read_portfolio().await?.positions)
975 }
976
977 async fn balances(&self) -> Result<Vec<Balance>, Error> {
978 Ok(self.read_portfolio().await?.balances)
979 }
980
981 async fn subscribe_portfolio(&self, opts: SubscribeOpts) -> Result<SubscriptionId, Error> {
982 let topics = vec![
983 position_topic(&self.inner.category),
984 WALLET_TOPIC.to_string(),
985 ];
986 let id = self
987 .register_ud_sub(UdSubKind::Portfolio, opts, topics)
988 .await?;
989 match self.read_portfolio().await {
992 Ok(p) => {
993 let mut events = Vec::new();
994 if !p.positions.is_empty() {
995 events.push(CoreEvent {
996 sub: Some(id),
997 payload: Event::Positions(p.positions),
998 });
999 }
1000 if !p.balances.is_empty() {
1001 events.push(CoreEvent {
1002 sub: Some(id),
1003 payload: Event::Balances(p.balances),
1004 });
1005 }
1006 self.inner.emitter.emit(events);
1007 Ok(id)
1008 }
1009 Err(e) => {
1010 self.inner.subs.write().await.remove(&id);
1011 self.drop_sub(id).await;
1012 Err(e)
1013 }
1014 }
1015 }
1016}
1017
1018#[async_trait]
1019impl StopOrderExecutor for UserDataConnector {
1020 async fn place_stop(&self, req: PlaceStopOrderRequest) -> Result<OrderAck, Error> {
1021 self.check_command().await?;
1022 let symbol = self.symbol(&req.instrument)?;
1024 self.inner
1025 .price_grid
1026 .check_qty(&req.instrument.key, req.qty)?;
1027 self.inner
1028 .price_grid
1029 .check(&req.instrument.key, req.stop_price)?;
1030 if let Some(p) = req.limit_price {
1031 self.inner.price_grid.check(&req.instrument.key, p)?;
1032 }
1033 let body = map_stop::build(&req, &symbol, &self.inner.category, req.cid)?;
1034 let rules = self.rules_of(&symbol).await?;
1035 rules.check_qty(req.qty)?;
1036 rules.check_price(req.stop_price)?;
1037 if let Some(p) = req.limit_price {
1038 rules.check_price(p)?;
1039 }
1040 self.acquire("PlaceStopOrder")?;
1042 let _instrument = self.inner.instrument_locks.lock(&req.instrument).await;
1043 self.check_command().await?;
1045 self.inner.instrument_locks.bind(req.cid, &req.instrument);
1046 self.begin_place(req.cid, "place_stop").await?;
1047 self.send_create(req.cid, &symbol, body).await
1048 }
1049
1050 async fn cancel_stop(&self, stop: StopOrderRef) -> Result<(), Error> {
1051 self.check_command().await?;
1052 self.acquire("CancelStopOrder")?;
1053 let (known_cid, order_id, symbol) = match stop {
1054 StopOrderRef::Cid(cid) => {
1055 let t = self.find_order(cid).await.map_err(|e| {
1056 if e.is_not_found() {
1057 Error::Rejected {
1058 code: brokapi_core::NOT_FOUND.into(),
1059 message: "the exchange has no stop order with this client order id"
1060 .into(),
1061 }
1062 } else {
1063 e
1064 }
1065 })?;
1066 (cid, t.order_id, t.symbol)
1067 }
1068 StopOrderRef::Broker(id) => {
1069 self.acquire("GetOrder")?;
1071 let page = self
1072 .inner
1073 .http
1074 .get_open_orders(
1075 &self.inner.category,
1076 None,
1077 Some(&self.inner.settle_coin),
1078 Some(id.as_str()),
1079 None,
1080 ORDERS_PAGE,
1081 None,
1082 )
1083 .await
1084 .map_err(map_err)?;
1085 let rec = page
1086 .list
1087 .into_iter()
1088 .next()
1089 .ok_or_else(|| Error::Rejected {
1090 code: brokapi_core::NOT_FOUND.into(),
1091 message: "the exchange has no stop order with this number".into(),
1092 })?;
1093 let cid = self
1094 .inner
1095 .core
1096 .lock()
1097 .await
1098 .resolve_cid(&rec.order_id, &rec.order_link_id);
1099 (cid, rec.order_id, rec.symbol)
1100 }
1101 };
1102 let _instrument = self.inner.instrument_locks.lock_cid(known_cid).await;
1103 self.check_command().await?;
1105 self.inner
1106 .http
1107 .cancel_order(&CancelOrderRequest {
1108 category: self.inner.category.clone(),
1109 symbol,
1110 order_id: Some(order_id),
1111 order_link_id: None,
1112 })
1113 .await
1114 .map_err(map_command_err)?;
1115 Ok(())
1116 }
1117
1118 async fn active_stop_orders(&self) -> Result<Vec<StopOrderSnapshot>, Error> {
1119 let records = self.all_open_orders().await?;
1120 let now = brokapi_runtime::now();
1121 let mut core = self.inner.core.lock().await;
1122 let mut out = Vec::new();
1123 for r in records.iter().filter(|r| is_untriggered(r)) {
1124 let cid = core.resolve_cid(&r.order_id, &r.order_link_id);
1125 self.inner.track(cid, &r.order_id, &r.symbol);
1126 if let Some(s) = map_stop::snapshot(r, Some(cid), &self.inner.category, now) {
1127 out.push(s);
1128 }
1129 }
1130 Ok(out)
1131 }
1132
1133 async fn subscribe_stop_orders(&self) -> Result<SubscriptionId, Error> {
1134 Err(Error::Unsupported {
1136 capability: "stop_order_stream".into(),
1137 })
1138 }
1139}