Skip to main content

brokapi_tbank/
ud.rs

1//! T-Bank user data connector, one account.
2//!
3//! [`UserDataConnector`] places and cancels orders, reports their state, and reads the portfolio,
4//! the own trades and the stop orders of ONE account.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9
10use async_trait::async_trait;
11use brokapi_core::{
12    Backpressure, Balance, BrokerOrderId, ClientOrderId, Envelope, Error, Execution,
13    ExecutionSource, InstrumentId, OrderAck, OrderExecutor, OrderSnapshot, OrderType,
14    PlaceIdempotency, PlaceOrderRequest, PlaceStopOrderRequest, Portfolio, PortfolioSource,
15    Position, ReconcileReport, ReplaceOrderRequest, ShutdownMode, ShutdownReport,
16    StopOrderExecutor, StopOrderRef, StopOrderSnapshot, SubscribeOpts, SubscriptionId, TimeRange,
17    UserDataCaps,
18};
19use brokapi_runtime::{
20    BrokerLimiter, CommandGate, InstrumentLocks, PriceGrid, ShutdownController, reconcile_ledger,
21};
22use brokapi_sansio::{BrokerCore, CoreEvent, EventSink};
23use brokapi_tbank_protocol::TbankClient;
24use brokapi_tbank_protocol::v1 as pb;
25use tokio::sync::{mpsc, oneshot};
26use tokio::task::JoinHandle;
27use tokio_util::sync::CancellationToken;
28use tracing::warn;
29
30use crate::config::{UD_MAX_PING, UserDataConfig, effective_ping, silence_limit};
31use crate::core::ud::{StreamKind, UdCore, UdFrame, UdSubKind};
32use crate::emit::Emitter;
33use crate::error::{STOP_ORDERS_RESPONSE_LIMIT, config, is_not_found, map_status, transport};
34use crate::ids::{
35    id_from_parts, instrument_ref, instrument_request, nanos_to_proto, price_to_quotation,
36    proto_to_nanos, require_native, stamps, wall_nanos,
37};
38use crate::lots::{LotBook, pieces_to_lots};
39use crate::map::{execution, order as map_order, portfolio as map_portfolio, stop as map_stop};
40use crate::raw::TbankRaw;
41use crate::stream::{LastError, supervise};
42use crate::util::{CommandOutcome, SubIdGen, command, query};
43
44/// Pages of operation history read at most per `executions` call (1000 operations each).
45const MAX_HISTORY_PAGES: usize = 20;
46
47/// The widest period T-Bank accepts in `from` and `to` (T-Invest errors 30061 and 30062): 1970-01-01 to 2099-12-31.
48const STOP_READ_FROM: i64 = 0;
49const STOP_READ_TO: i64 = 4_102_444_799;
50/// Requests `active_stop_orders` may spend on one list when the broker keeps answering 30261.
51const STOP_READ_MAX_REQUESTS: usize = 40;
52
53fn stop_list_too_long(requests: usize) -> Error {
54    Error::Rejected {
55        code: STOP_ORDERS_RESPONSE_LIMIT.to_string(),
56        message: format!(
57            "30261: the broker refuses to list the active stop orders, even in pieces ({requests} requests). \
58             Cancel stop orders you do not need, or read them by period with raw()"
59        ),
60    }
61}
62
63/// Stop orders this connector placed: the broker's `StopOrder` does not carry our idempotency key,
64/// so the cid <-> broker id link lives only here (lost on restart, see the T-Bank page of the book).
65///
66/// Plus the stop orders whose placement ended without a clear answer ([`LostStop`]): the broker may
67/// hold them, and the link is made later, from the list, when the stop order can be told for sure.
68#[derive(Default)]
69struct StopBook {
70    /// `None` = sent, the broker's id is not known (yet).
71    by_cid: HashMap<ClientOrderId, Option<String>>,
72    by_id: HashMap<String, ClientOrderId>,
73    /// The cids in the order they were sent, for the window.
74    order: std::collections::VecDeque<ClientOrderId>,
75    /// Placements without a clear answer that wait to be found in the list, oldest first.
76    lost: Vec<LostStop>,
77}
78
79/// How many stop-order cids the book remembers besides the live ones. The guarantee "a cid is sent
80/// once" holds inside this window.
81const STOP_BOOK_CAP: usize = 10_000;
82
83/// How many placements without an answer wait to be found in the list. When there are more, the oldest
84/// are forgotten (their stop orders then show `cid: None`).
85const LOST_STOP_CAP: usize = 64;
86
87/// How far the clock of the broker may differ from ours when a lost placement is matched with a stop order
88/// by its creation time. The broker writes the time in whole seconds, and the two clocks are kept in step by
89/// the network time, so a few seconds is wider than any honest difference. A wider margin would only
90/// let stop orders placed by someone else a minute apart look like ours: the margin is the width of the window in
91/// which "the same stop order" is still a guess that can be right.
92const STOP_CLOCK_SKEW: Duration = Duration::from_secs(10);
93
94/// A `place_stop` that ended with [`Error::Unknown`] (or with an answer that has no stop order id): the
95/// request and the time it was in flight. T-Bank does not return our `cid` in the list, so the stop order
96/// is recognised by what was asked for and when it was created ([`StopBook::adopt`]).
97struct LostStop {
98    req: PlaceStopOrderRequest,
99    lots: i64,
100    /// Our wall clock just before the request left.
101    sent: i64,
102    /// Our wall clock when the wait for the answer ended.
103    gave_up: i64,
104}
105
106impl LostStop {
107    fn skew() -> i64 {
108        i64::try_from(STOP_CLOCK_SKEW.as_nanos()).unwrap_or(i64::MAX)
109    }
110
111    /// The broker stop order `o` is what was asked for and was created while the request could be at the
112    /// broker, give or take the difference of the clocks.
113    fn could_be(&self, o: &pb::StopOrder) -> bool {
114        let Some(created) = o
115            .create_date
116            .as_ref()
117            .filter(|t| t.seconds != 0)
118            .map(|t| proto_to_nanos(t).as_nanos())
119        else {
120            return false;
121        };
122        created >= self.sent.saturating_sub(Self::skew())
123            && created <= self.gave_up.saturating_add(Self::skew())
124            && map_stop::same_as_request(o, &self.req, self.lots)
125    }
126
127    /// After this moment (our clock) a stop order created by this request would already be in the list:
128    /// a read that comes later and finds nothing means there is nothing to find.
129    fn settled_at(&self) -> i64 {
130        self.gave_up.saturating_add(Self::skew())
131    }
132}
133
134impl StopBook {
135    /// `false` when this cid was already sent: the library never sends the same stop order twice. A repeat
136    /// does not touch the record of the first send (it may already hold the broker's id).
137    fn begin(&mut self, cid: ClientOrderId) -> bool {
138        if self.by_cid.contains_key(&cid) {
139            return false;
140        }
141        self.by_cid.insert(cid, None);
142        self.order.push_back(cid);
143        self.trim();
144        true
145    }
146
147    /// Keeps the book bounded: a cid that has no live stop order behind it (`None`, or the id of a stop
148    /// that was cancelled) is forgotten after [`STOP_BOOK_CAP`] newer ones. A stop order that is still
149    /// live is never dropped by this.
150    fn trim(&mut self) {
151        let mut checked = 0;
152        while self.order.len() > STOP_BOOK_CAP && checked < self.order.len() {
153            let Some(old) = self.order.pop_front() else {
154                break;
155            };
156            checked += 1;
157            let live = self
158                .by_cid
159                .get(&old)
160                .and_then(|id| id.as_ref())
161                .is_some_and(|id| self.by_id.contains_key(id));
162            if live {
163                self.order.push_back(old);
164            } else {
165                self.by_cid.remove(&old);
166            }
167        }
168    }
169
170    fn bind(&mut self, cid: ClientOrderId, id: &str) {
171        self.by_cid.insert(cid, Some(id.to_string()));
172        self.by_id.insert(id.to_string(), cid);
173    }
174
175    /// The broker refused for sure: the cid is clean again.
176    fn forget_cid(&mut self, cid: ClientOrderId) {
177        self.by_cid.remove(&cid);
178    }
179
180    /// The stop order is gone (cancelled, triggered). Its cid stays used: `by_cid` keeps the record, so a
181    /// repeat of the same cid is still refused, and the cid is dropped only by the window of [`trim`].
182    fn forget_id(&mut self, id: &str) {
183        self.by_id.remove(id);
184    }
185
186    fn id_of(&self, cid: ClientOrderId) -> Option<String> {
187        self.by_cid.get(&cid).cloned().flatten()
188    }
189
190    fn cid_of(&self, id: &str) -> Option<ClientOrderId> {
191        self.by_id.get(id).copied()
192    }
193
194    /// Remembers a placement that ended without a clear answer. `sent` and `gave_up` are wall-clock
195    /// nanoseconds before the request left and when the wait for the answer ended.
196    fn lose(&mut self, req: PlaceStopOrderRequest, lots: i64, sent: i64, gave_up: i64) {
197        self.lost.push(LostStop {
198            req,
199            lots,
200            sent,
201            gave_up,
202        });
203        if self.lost.len() > LOST_STOP_CAP {
204            self.lost.remove(0);
205        }
206    }
207
208    /// Links the active stop orders `raw` (the whole list, as the broker returned it, `now` is our wall clock in
209    /// nanoseconds) with the placements that got no answer, and forgets the placements that can no longer be found.
210    ///
211    /// A stop order with no cid of ours is linked to a placement only when both are the only match of each
212    /// other: the stop order fits that placement and no other one, and the placement fits no other stop order
213    /// that has no cid. Two equal placements, or one placement and two equal stop orders (one of them may be
214    /// placed by hand), link nothing: the connector does not guess. A placement is forgotten when it is linked, or
215    /// when a read made after its window finds no match (it was never placed, or is gone already).
216    fn adopt(&mut self, raw: &[pb::StopOrder], now: i64) {
217        let free: Vec<&pb::StopOrder> = raw
218            .iter()
219            .filter(|o| !self.by_id.contains_key(&o.stop_order_id))
220            .collect();
221        let mut links = Vec::new();
222        for (i, lost) in self.lost.iter().enumerate() {
223            let mut fits = free.iter().filter(|o| lost.could_be(o));
224            let (Some(o), None) = (fits.next(), fits.next()) else {
225                continue;
226            };
227            if self.lost.iter().filter(|other| other.could_be(o)).count() == 1 {
228                links.push((i, o.stop_order_id.clone()));
229            }
230        }
231        let mut linked = vec![false; self.lost.len()];
232        for (i, id) in links {
233            linked[i] = true;
234            let cid = self.lost[i].req.cid;
235            self.bind(cid, &id);
236        }
237        let mut i = 0;
238        self.lost.retain(|lost| {
239            i += 1;
240            !linked[i - 1] && now <= lost.settled_at()
241        });
242    }
243}
244
245struct Shared {
246    client: TbankClient,
247    limiter: Arc<BrokerLimiter>,
248    emitter: Arc<Emitter>,
249    account: String,
250    core: Mutex<UdCore>,
251    /// Lot sizes: the same book the core reads (the messages count in lots, the events in units).
252    lots: Arc<LotBook>,
253    /// Commands on one instrument go one at a time, in arrival order.
254    locks: InstrumentLocks,
255    price_grid: PriceGrid,
256    stops: Mutex<StopBook>,
257    ctl: Mutex<ShutdownController>,
258    waiters: Mutex<HashMap<SubscriptionId, oneshot::Sender<Result<(), String>>>>,
259    sub_ids: SubIdGen,
260    streams: Mutex<HashMap<StreamKind, (CancellationToken, JoinHandle<()>)>>,
261    frames: mpsc::Sender<UdFrame>,
262    last_error: Arc<LastError>,
263    request_timeout: Duration,
264    ping: Duration,
265    order_state_buffer: usize,
266    sandbox: bool,
267    cancel: CancellationToken,
268}
269
270impl Shared {
271    fn with_core<R>(&self, f: impl FnOnce(&mut UdCore, &mut EventSink) -> R) -> R {
272        let mut sink = EventSink::new();
273        let (r, reconnected) = {
274            let mut core = self.core.lock().expect("ud core poisoned");
275            let r = f(&mut core, &mut sink);
276            for u in core.take_updates() {
277                if let Some(tx) = self
278                    .waiters
279                    .lock()
280                    .expect("waiters poisoned")
281                    .remove(&u.sub)
282                {
283                    let _ = tx.send(u.result);
284                }
285            }
286            for cid in core.take_forgotten() {
287                self.locks.forget(cid);
288            }
289            (r, core.take_reconnected())
290        };
291        self.emitter.emit(sink.drain());
292        for kind in reconnected {
293            if kind == StreamKind::OrderState {
294                self.refresh_orders();
295            }
296        }
297        r
298    }
299
300    /// After the order stream reopened: orders may have changed meanwhile, so re-read the live ones
301    /// and let the core emit what differs. Orders that ended during the gap are not in `GetOrders`:
302    /// the client finds them with `order_state` / `executions` (documented on the T-Bank page of the book).
303    fn refresh_orders(&self) {
304        let (client, limiter, tx) = (
305            self.client.clone(),
306            Arc::clone(&self.limiter),
307            self.frames.clone(),
308        );
309        let (account, timeout, cancel) = (
310            self.account.clone(),
311            self.request_timeout,
312            self.cancel.child_token(),
313        );
314        tokio::spawn(async move {
315            if limiter
316                .acquire("GetOrders", brokapi_runtime::now())
317                .is_err()
318            {
319                return;
320            }
321            let mut c = client.orders();
322            let req = pb::GetOrdersRequest {
323                account_id: account,
324                advanced_filters: None,
325            };
326            let res = tokio::select! {
327                _ = cancel.cancelled() => return,
328                r = query(timeout, c.get_orders(req)) => r,
329            };
330            match res {
331                Ok(r) => {
332                    let _ = tx.send(UdFrame::ActiveOrders(r)).await;
333                }
334                Err(e) => warn!(error = %e, "order refresh after reconnect failed"),
335            }
336        });
337    }
338
339    /// The lot size of an instrument: from the book, else one `GetInstrumentBy`, remembered.
340    async fn lot_of(&self, id: &InstrumentId) -> Result<i64, Error> {
341        require_native(id)?;
342        if let Some(lot) = self.lots.get(id) {
343            return Ok(lot);
344        }
345        self.limiter
346            .acquire("GetInstrumentBy", brokapi_runtime::now())?;
347        let mut c = self.client.instruments();
348        let r = query(
349            self.request_timeout,
350            c.get_instrument_by(instrument_request(id)),
351        )
352        .await?;
353        let i = r.instrument.ok_or_else(|| {
354            Error::not_found(format!("instrument {} not found", id.key.display()))
355        })?;
356        self.lots.put_instrument(id, &i);
357        Ok(i64::from(i.lot).max(1))
358    }
359
360    /// Learns the lot size of the instruments a frame is about, before the sans-io core reads it.
361    async fn prefetch_lots(&self, frame: &UdFrame) -> Result<(), Error> {
362        use pb::order_state_stream_response::Payload;
363        let mut wanted: Vec<InstrumentId> = Vec::new();
364        match frame {
365            UdFrame::OrderState(r) => {
366                if let Some(Payload::OrderState(o)) = &r.payload
367                    && o.lot_size <= 0
368                {
369                    wanted.push(id_from_parts(
370                        &o.instrument_uid,
371                        "",
372                        &o.ticker,
373                        &o.class_code,
374                    ));
375                }
376            }
377            UdFrame::ActiveOrders(r) => {
378                for o in &r.orders {
379                    wanted.push(id_from_parts(
380                        &o.instrument_uid,
381                        &o.figi,
382                        &o.ticker,
383                        &o.class_code,
384                    ));
385                }
386            }
387            _ => {}
388        }
389        for id in wanted {
390            let known = match &id.native {
391                brokapi_core::NativeId::Uid(u) => {
392                    self.lots.get_parts(u, "", &id.key.symbol, &id.key.mic)
393                }
394                _ => self.lots.get(&id),
395            };
396            if known.is_none() {
397                self.lot_of(&id).await?;
398            }
399        }
400        Ok(())
401    }
402
403    /// A frame could not be processed (its lot size is unknown): the order events in it are lost.
404    /// Say so as an overflow does: the gate closes until `reconcile`.
405    fn frame_lost(&self, frame: &UdFrame) {
406        let kind = match frame {
407            UdFrame::OrderState(_) | UdFrame::ActiveOrders(_) => UdSubKind::OrderState,
408            _ => return,
409        };
410        self.emitter.mark_degraded();
411        if let Some(sub) = self.core.lock().expect("ud core poisoned").sub(kind) {
412            self.emitter.emit(vec![CoreEvent {
413                sub: Some(sub),
414                payload: brokapi_core::Event::Overflow {
415                    sub,
416                    lost_from: crate::ids::wall_nanos(),
417                },
418            }]);
419        }
420    }
421
422    async fn pump(self: Arc<Self>, mut rx: mpsc::Receiver<UdFrame>) {
423        // The safety net for a placement whose task ended without an answer (it normally cannot: see
424        // `place`). Every few seconds the placements that waited too long become `Unknown`.
425        let mut sweep = tokio::time::interval(Duration::from_secs(5));
426        sweep.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
427        loop {
428            let backlog = self.emitter.has_backlog();
429            tokio::select! {
430                _ = self.cancel.cancelled() => return,
431                _ = sweep.tick() => {
432                    self.with_core(|c, s| c.sweep_timeouts(brokapi_runtime::now(), s));
433                }
434                frame = rx.recv() => {
435                    let Some(frame) = frame else { return };
436                    if let UdFrame::StreamFailed(kind, reason) = &frame {
437                        warn!(?kind, %reason, "stream refused for good");
438                        // A dead stream of orders or fills is a hole that never closes: no trading until the
439                        // user has looked (and reconnected with a new connector).
440                        if matches!(kind, StreamKind::OrderState | StreamKind::Trades) {
441                            self.emitter.mark_degraded();
442                        }
443                    }
444                    if let Err(e) = self.prefetch_lots(&frame).await {
445                        warn!(error = %e, "lot size of an instrument is not known: order events lost");
446                        self.frame_lost(&frame);
447                        continue;
448                    }
449                    self.with_core(|c, s| c.on_frame(frame, brokapi_runtime::now(), s));
450                }
451                _ = tokio::time::sleep(Duration::from_millis(20)), if backlog => {
452                    self.emitter.flush();
453                }
454            }
455        }
456    }
457
458    fn start_stream(&self, kind: StreamKind) {
459        let mut streams = self.streams.lock().expect("streams poisoned");
460        // A supervisor that gave up (the broker refused the stream for good) leaves a finished handle: a
461        // new subscription tries again.
462        if streams.get(&kind).is_some_and(|(_, h)| !h.is_finished()) {
463            return;
464        }
465        let token = self.cancel.child_token();
466        let (client, tx, last_error) = (
467            self.client.clone(),
468            self.frames.clone(),
469            Arc::clone(&self.last_error),
470        );
471        let accounts = vec![self.account.clone()];
472        let ms = self.ping.as_millis() as i32;
473        let stale = silence_limit(self.ping);
474        let t = token.clone();
475        let handle = match kind {
476            StreamKind::OrderState => {
477                let open = move || {
478                    let mut c = client.orders_stream();
479                    let req = pb::OrderStateStreamRequest {
480                        accounts: accounts.clone(),
481                        ping_delay_millis: Some(ms),
482                    };
483                    async move { c.order_state_stream(req).await.map(|r| r.into_inner()) }
484                };
485                tokio::spawn(supervise(
486                    "order_state",
487                    open,
488                    move || UdFrame::Connected(kind),
489                    UdFrame::OrderState,
490                    move |reason| UdFrame::StreamFailed(kind, reason),
491                    tx,
492                    stale,
493                    last_error,
494                    t,
495                ))
496            }
497            StreamKind::Trades => {
498                let open = move || {
499                    let mut c = client.orders_stream();
500                    let req = pb::TradesStreamRequest {
501                        accounts: accounts.clone(),
502                        ping_delay_ms: Some(ms),
503                    };
504                    async move { c.trades_stream(req).await.map(|r| r.into_inner()) }
505                };
506                tokio::spawn(supervise(
507                    "trades",
508                    open,
509                    move || UdFrame::Connected(kind),
510                    UdFrame::Trades,
511                    move |reason| UdFrame::StreamFailed(kind, reason),
512                    tx,
513                    stale,
514                    last_error,
515                    t,
516                ))
517            }
518            StreamKind::Portfolio => {
519                let open = move || {
520                    let mut c = client.operations_stream();
521                    let req = pb::PortfolioStreamRequest {
522                        accounts: accounts.clone(),
523                        ping_settings: Some(pb::PingDelaySettings {
524                            ping_delay_ms: Some(ms),
525                        }),
526                    };
527                    async move { c.portfolio_stream(req).await.map(|r| r.into_inner()) }
528                };
529                tokio::spawn(supervise(
530                    "portfolio",
531                    open,
532                    move || UdFrame::Connected(kind),
533                    UdFrame::Portfolio,
534                    move |reason| UdFrame::StreamFailed(kind, reason),
535                    tx,
536                    stale,
537                    last_error,
538                    t,
539                ))
540            }
541            StreamKind::Positions => {
542                let open = move || {
543                    let mut c = client.operations_stream();
544                    let req = pb::PositionsStreamRequest {
545                        accounts: accounts.clone(),
546                        with_initial_positions: true,
547                        ping_settings: Some(pb::PingDelaySettings {
548                            ping_delay_ms: Some(ms),
549                        }),
550                    };
551                    async move { c.positions_stream(req).await.map(|r| r.into_inner()) }
552                };
553                tokio::spawn(supervise(
554                    "positions",
555                    open,
556                    move || UdFrame::Connected(kind),
557                    UdFrame::Positions,
558                    move |reason| UdFrame::StreamFailed(kind, reason),
559                    tx,
560                    stale,
561                    last_error,
562                    t,
563                ))
564            }
565        };
566        streams.insert(kind, (token, handle));
567    }
568
569    fn stop_stream(&self, kind: StreamKind) {
570        if let Some((token, handle)) = self.streams.lock().expect("streams poisoned").remove(&kind)
571        {
572            token.cancel();
573            handle.abort();
574        }
575    }
576}
577
578/// T-Bank user data connector: orders, own trades, portfolio and stop orders of ONE account.
579///
580/// It implements [`OrderExecutor`], [`ExecutionSource`], [`PortfolioSource`] and
581/// [`StopOrderExecutor`]. Commands return their answer directly. Order states, own trades and
582/// portfolio changes arrive as [`Envelope`]s in the channel given to [`start`](Self::start), after
583/// you subscribe.
584///
585/// # Rules that protect your money
586///
587/// - **The gate.** Trading commands return [`Error::NotReconciled`] until [`OrderExecutor::reconcile`]
588///   has succeeded once. Call it right after `start` with the cids you have on record (an empty
589///   slice if none).
590/// - **A place is never repeated.** If a `place` gets no answer (timeout, broken connection, server
591///   fault) the result is [`Error::Unknown`] and the library does NOT send it again, because a retry
592///   could create a second order. A `cid` is used for one `place` at most: sending it again is
593///   refused locally as `duplicate_place`. Look at the order with
594///   [`order_state`](OrderExecutor::order_state), and use a NEW `cid` for a new attempt.
595/// - **One account.** Messages for another account are dropped. For several accounts of one token
596///   start one connector per account and share ONE rate limiter between them.
597/// - **Units, not lots.** Quantities are in units of the instrument (shares, bonds), as at every broker. T-Bank counts
598///   in lots: the connector converts both ways with the lot size of the instrument (read once, from the directory). An
599///   order that is not a whole number of lots is refused with `invalid_qty` before it is sent. Prices are per one instrument.
600///
601/// # Examples
602///
603/// Place an order, cancel it, and see a refusal. The mock refuses a limit order at one marker price
604/// (13.37), where a real broker refuses a price outside its band.
605///
606/// ```
607/// use brokapi_core::{
608///     ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
609///     Price, Qty, ShutdownMode, Side,
610/// };
611/// use brokapi_runtime::BrokerLimiter;
612/// use brokapi_tbank::limits::default_limits;
613/// use brokapi_tbank::{UserDataConfig, UserDataConnector};
614/// use tokio::sync::mpsc;
615/// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
616/// # use brokapi_tbank_protocol::TbankConfig;
617///
618/// # #[tokio::main(flavor = "current_thread")]
619/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
620/// # let server = MockServer::start(Default::default()).await;
621/// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
622/// # let refused_price = brokapi_conformance::REJECT_PRICE;
623/// let (events_tx, _events) = mpsc::channel(256);
624/// let limiter = BrokerLimiter::new(&default_limits());
625/// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud-main"), limiter, events_tx).await?;
626///
627/// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
628/// let buy = |cid, price| PlaceOrderRequest {
629///     cid,
630///     instrument: sber.clone(),
631///     side: Side::Buy,
632///     order_type: OrderType::Limit,
633///     qty: Qty::from(10), // one lot of SBER: 10 shares
634///     price: Some(price),
635/// };
636///
637/// // 1. The gate is closed until the first reconcile. Nothing was sent.
638/// let early = ud.place(buy(ClientOrderId::new_v4(), Price::from(250))).await;
639/// assert!(matches!(early, Err(Error::NotReconciled)));
640///
641/// // 2. Reconcile with the cids you know. A fresh start knows none.
642/// let report = ud.reconcile(&[]).await?;
643/// assert!(report.confirmed.is_empty());
644///
645/// // 3. Place, look, cancel.
646/// let cid = ClientOrderId::new_v4();
647/// let ack = ud.place(buy(cid, Price::from(250))).await?;
648/// assert_eq!(ack.cid, cid);
649/// assert_eq!(ud.active_orders().await?.len(), 1);
650/// ud.cancel(cid).await?;
651/// assert!(ud.active_orders().await?.is_empty());
652///
653/// // 4. A refusal is `Rejected`. The broker did nothing, and the next order is not blocked.
654/// let refused = ud.place(buy(ClientOrderId::new_v4(), refused_price)).await;
655/// assert!(matches!(refused, Err(Error::Rejected { .. })));
656/// assert!(ud.place(buy(ClientOrderId::new_v4(), Price::from(251))).await.is_ok());
657///
658/// ud.shutdown(ShutdownMode::Immediate).await;
659/// # Ok(())
660/// # }
661/// ```
662///
663/// # Several accounts
664///
665/// One token can see several accounts. Start one connector per account. Give each its own `source`
666/// and share ONE limiter, because T-Bank counts its limits per user.
667///
668/// ```
669/// use std::sync::Arc;
670/// use brokapi_core::{OrderExecutor, ShutdownMode};
671/// use brokapi_runtime::BrokerLimiter;
672/// use brokapi_tbank::limits::default_limits;
673/// use brokapi_tbank::{UserDataConfig, UserDataConnector};
674/// use brokapi_tbank_protocol::TbankClient;
675/// use tokio::sync::mpsc;
676/// # use brokapi_mocks::tbank::{MockServer, TOKEN};
677/// # use brokapi_tbank_protocol::TbankConfig;
678///
679/// # #[tokio::main(flavor = "current_thread")]
680/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
681/// # let server = MockServer::start(Default::default()).await;
682/// # let token_config = TbankConfig::for_mock(TOKEN, &server.endpoint);
683/// // `token_config` is your sandbox or production config without an account.
684/// let accounts = TbankClient::new(&token_config)?.list_accounts().await?;
685///
686/// let limiter = BrokerLimiter::new(&default_limits()); // ONE limiter for all accounts
687/// let mut connectors = Vec::new();
688/// for account in &accounts {
689///     let (events_tx, _events) = mpsc::channel(256);
690///     let cfg = UserDataConfig::new(
691///         token_config.clone().with_account(account.id.clone()),
692///         format!("tbank-ud-{}", account.id),
693///     );
694///     let ud = UserDataConnector::start(cfg, Arc::clone(&limiter), events_tx).await?;
695///     ud.reconcile(&[]).await?;
696///     connectors.push(ud);
697/// }
698/// assert_eq!(connectors.len(), accounts.len());
699/// assert_ne!(connectors[0].account_id(), connectors[1].account_id());
700/// for ud in connectors {
701///     ud.shutdown(ShutdownMode::Immediate).await;
702/// }
703/// # Ok(())
704/// # }
705/// ```
706pub struct UserDataConnector {
707    inner: Arc<Shared>,
708    pump: JoinHandle<()>,
709}
710
711impl UserDataConnector {
712    /// How many commands are in flight: sent, and the answer has not been taken yet. Zero when all of them
713    /// are settled. A `place` runs to its end even if the caller drops its future, so this number goes back
714    /// to zero by itself. For health checks and tests.
715    ///
716    /// # Examples
717    ///
718    /// ```
719    /// use brokapi_core::ShutdownMode;
720    /// use brokapi_runtime::BrokerLimiter;
721    /// use brokapi_tbank::limits::default_limits;
722    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
723    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
724    /// # use brokapi_tbank_protocol::TbankConfig;
725    ///
726    /// # #[tokio::main(flavor = "current_thread")]
727    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
728    /// # let server = MockServer::start(Default::default()).await;
729    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
730    /// let (events_tx, _events) = tokio::sync::mpsc::channel(16);
731    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
732    /// assert_eq!(ud.in_flight_orders(), 0);
733    /// ud.shutdown(ShutdownMode::Immediate).await;
734    /// # Ok(())
735    /// # }
736    /// ```
737    pub fn in_flight_orders(&self) -> usize {
738        self.inner
739            .core
740            .lock()
741            .expect("ud core poisoned")
742            .orders_mut()
743            .in_flight_count()
744    }
745
746    /// Resolves the account (`cfg.tbank.account_id`, or the token's only open account) and starts.
747    /// Commands are refused with `NotReconciled` until [`OrderExecutor::reconcile`] succeeds.
748    ///
749    /// With an `account_id` in the config nothing is sent at start. Without one the connector calls
750    /// `GetAccounts` once, and takes the only open account. Streams are opened later, when you
751    /// subscribe. `limiter` should be the one limiter of the whole user (see [`crate::limits`]).
752    ///
753    /// # Errors
754    ///
755    /// - [`Error::Config`] when the token or endpoint is invalid, when no `account_id` is set and the
756    ///   token has no open account, when it has several (set `tbank.account_id`), or when the
757    ///   broker does not accept the token.
758    /// - [`Error::Transport`] when the `GetAccounts` call fails on the network (only without an
759    ///   `account_id`), and [`Error::RateLimited`] when it is refused for rate.
760    ///
761    /// # Examples
762    ///
763    /// ```
764    /// use brokapi_core::{Error, ShutdownMode};
765    /// use brokapi_runtime::BrokerLimiter;
766    /// use brokapi_tbank::limits::default_limits;
767    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
768    /// use tokio::sync::mpsc;
769    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
770    /// # use brokapi_tbank_protocol::TbankConfig;
771    ///
772    /// # #[tokio::main(flavor = "current_thread")]
773    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
774    /// # let server = MockServer::start(Default::default()).await;
775    /// # let token_config = TbankConfig::for_mock(TOKEN, &server.endpoint);
776    /// let limiter = BrokerLimiter::new(&default_limits());
777    /// let (events_tx, _events) = mpsc::channel(256);
778    ///
779    /// // This token sees two accounts, so the connector will not guess: it refuses to start.
780    /// let cfg = UserDataConfig::new(token_config.clone(), "tbank-ud");
781    /// let result = UserDataConnector::start(cfg, limiter.clone(), events_tx.clone()).await;
782    /// assert!(matches!(result, Err(Error::Config(_))));
783    ///
784    /// // Name the account and it starts.
785    /// let cfg = UserDataConfig::new(token_config.with_account(ACCOUNT_A), "tbank-ud");
786    /// let ud = UserDataConnector::start(cfg, limiter, events_tx).await?;
787    /// assert_eq!(ud.account_id(), ACCOUNT_A);
788    /// ud.shutdown(ShutdownMode::Immediate).await;
789    /// # Ok(())
790    /// # }
791    /// ```
792    pub async fn start(
793        cfg: UserDataConfig,
794        limiter: Arc<BrokerLimiter>,
795        out: mpsc::Sender<Envelope>,
796    ) -> Result<Self, Error> {
797        let client = TbankClient::new(&cfg.tbank).map_err(|e| config(e.to_string()))?;
798        let account = match cfg.tbank.account_id.clone() {
799            Some(a) => a,
800            None => pick_account(&client, cfg.tbank.request_timeout).await?,
801        };
802        let (frames, rx) = mpsc::channel(1024);
803        let ctl = ShutdownController::new(CommandGate::new());
804        let cancel = ctl.child_token();
805        let mut core = UdCore::new(account.clone());
806        // A placement is given up after its request has certainly ended.
807        core.set_command_timeout(cfg.tbank.request_timeout + Duration::from_secs(10));
808        let lots = Arc::clone(core.lots());
809        let inner = Arc::new(Shared {
810            client,
811            limiter,
812            emitter: Emitter::new(cfg.source, out),
813            core: Mutex::new(core),
814            lots,
815            locks: InstrumentLocks::new(),
816            price_grid: cfg.price_grid.clone(),
817            stops: Mutex::new(StopBook::default()),
818            account,
819            ctl: Mutex::new(ctl),
820            waiters: Mutex::new(HashMap::new()),
821            sub_ids: SubIdGen::new(1000),
822            streams: Mutex::new(HashMap::new()),
823            frames,
824            last_error: Arc::new(LastError::default()),
825            request_timeout: cfg.tbank.request_timeout,
826            ping: effective_ping(cfg.ping_interval, UD_MAX_PING),
827            order_state_buffer: cfg.order_state_buffer,
828            sandbox: cfg.tbank.sandbox,
829            cancel,
830        });
831        let pump = tokio::spawn(Arc::clone(&inner).pump(rx));
832        Ok(Self { inner, pump })
833    }
834
835    /// The brokerage account this connector works on: the configured one, or the token's only open
836    /// account.
837    ///
838    /// # Examples
839    ///
840    /// ```
841    /// use brokapi_core::ShutdownMode;
842    /// use brokapi_runtime::BrokerLimiter;
843    /// use brokapi_tbank::limits::default_limits;
844    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
845    /// use tokio::sync::mpsc;
846    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_B, TOKEN};
847    /// # use brokapi_tbank_protocol::TbankConfig;
848    ///
849    /// # #[tokio::main(flavor = "current_thread")]
850    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
851    /// # let server = MockServer::start(Default::default()).await;
852    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_B);
853    /// let (events_tx, _events) = mpsc::channel(16);
854    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
855    /// assert_eq!(ud.account_id(), ACCOUNT_B);
856    /// # ud.shutdown(ShutdownMode::Immediate).await;
857    /// # Ok(())
858    /// # }
859    /// ```
860    pub fn account_id(&self) -> &str {
861        &self.inner.account
862    }
863
864    /// Any other method of the T-Bank API with this connector's token, endpoint and rate limiter.
865    /// `account_id()` of the result is this connector's account. See [`TbankRaw`].
866    ///
867    /// The raw layer does not know the connector's order registry: orders placed through it are not
868    /// tracked (no events, no `reconcile`). Use the trait methods for orders. Use `call_command`
869    /// for anything that changes the account, so a lost answer is [`Error::Unknown`].
870    ///
871    /// # Examples
872    ///
873    /// ```
874    /// use brokapi_runtime::BrokerLimiter;
875    /// use brokapi_tbank::limits::default_limits;
876    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
877    /// use brokapi_core::ShutdownMode;
878    /// use brokapi_tbank_protocol::v1 as pb;
879    /// use tokio::sync::mpsc;
880    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
881    /// # use brokapi_tbank_protocol::TbankConfig;
882    ///
883    /// # #[tokio::main(flavor = "current_thread")]
884    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
885    /// # let server = MockServer::start(Default::default()).await;
886    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
887    /// let (events_tx, _events) = mpsc::channel(16);
888    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
889    /// let raw = ud.raw();
890    /// assert_eq!(raw.account_id(), Some(ACCOUNT_A));
891    /// let account = ud.account_id().to_string();
892    /// // A read of the portfolio, spelled out through the raw layer.
893    /// let portfolio = raw
894    ///     .call("GetPortfolio", |c| async move {
895    ///         c.operations().get_portfolio(pb::PortfolioRequest { account_id: account, currency: None }).await
896    ///     })
897    ///     .await?;
898    /// assert!(!portfolio.positions.is_empty());
899    /// # ud.shutdown(ShutdownMode::Immediate).await;
900    /// # Ok(())
901    /// # }
902    /// ```
903    pub fn raw(&self) -> TbankRaw {
904        TbankRaw::new(
905            self.inner.client.clone(),
906            Arc::clone(&self.inner.limiter),
907            self.inner.request_timeout,
908            Some(self.inner.account.clone()),
909        )
910    }
911
912    /// The price steps this connector checks order prices against. Fill it from
913    /// [`TradingParams`](brokapi_core::TradingParams) (`grid.learn(&params)`) to have a price off the
914    /// step refused before anything is sent. See [`PriceGrid`].
915    pub fn price_grid(&self) -> &PriceGrid {
916        &self.inner.price_grid
917    }
918
919    /// What this connector can do.
920    ///
921    /// `place_idempotency` is `LabelOnly` on purpose: `order_id` is documented as an idempotency key
922    /// ("для целей идемпотентности"; error 30057 mentions a duplicate with a found report), but
923    /// nobody has measured the broker's behavior on a repeated key, and the library requires `LabelOnly`
924    /// until it is confirmed on the sandbox (a live run). The library never repeats a
925    /// `place` itself regardless.
926    ///
927    /// Replace and stop orders are supported, and own trades are streamed. There is no websocket RPC
928    /// and no ordered command channel: commands are plain gRPC calls, serialized per instrument by
929    /// the connector. `sandbox` is the `sandbox` flag of the config.
930    ///
931    /// # Examples
932    ///
933    /// ```
934    /// use brokapi_core::{PlaceIdempotency, ShutdownMode};
935    /// use brokapi_runtime::BrokerLimiter;
936    /// use brokapi_tbank::limits::default_limits;
937    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
938    /// use tokio::sync::mpsc;
939    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
940    /// # use brokapi_tbank_protocol::TbankConfig;
941    ///
942    /// # #[tokio::main(flavor = "current_thread")]
943    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
944    /// # let server = MockServer::start(Default::default()).await;
945    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
946    /// let (events_tx, _events) = mpsc::channel(16);
947    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
948    /// let caps = ud.caps();
949    /// assert_eq!(caps.place_idempotency, PlaceIdempotency::LabelOnly);
950    /// assert!(caps.supports_replace && caps.supports_stop_orders && caps.streams_executions);
951    /// assert!(caps.sandbox, "a mock config is a sandbox config");
952    /// # ud.shutdown(ShutdownMode::Immediate).await;
953    /// # Ok(())
954    /// # }
955    /// ```
956    pub fn caps(&self) -> UserDataCaps {
957        // Stop orders: StopOrdersService, through the `StopOrderExecutor` trait.
958        UserDataCaps::new(
959            PlaceIdempotency::LabelOnly,
960            false,
961            false,
962            true,
963            true,
964            true,
965            self.inner.sandbox,
966        )
967    }
968
969    /// Stops the connector. Orders already at the exchange are NOT touched.
970    ///
971    /// The command gate closes first. Placements still in flight are abandoned locally: each becomes
972    /// `Unknown` with one event, and its `cid` is listed in `inflight_abandoned`. Look at those orders
973    /// with a new connector (`reconcile`, `order_state`) before you place anything again.
974    ///
975    /// With [`ShutdownMode::Graceful`] the open server streams are closed (the broker has no
976    /// unsubscribe message, ending the call is the way) and `unsubscribed` counts these streams (a
977    /// portfolio subscription uses two). The event backlog gets until the deadline to drain into your
978    /// channel. With [`ShutdownMode::Immediate`] nothing is waited for and `unsubscribed` is 0.
979    /// `unsubscribe_failed` is always 0 here. The method consumes the connector.
980    ///
981    /// # Examples
982    ///
983    /// ```
984    /// use std::time::Duration;
985    /// use brokapi_core::{ExecutionSource, OrderExecutor, ShutdownMode};
986    /// use brokapi_runtime::BrokerLimiter;
987    /// use brokapi_tbank::limits::default_limits;
988    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
989    /// use tokio::sync::mpsc;
990    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
991    /// # use brokapi_tbank_protocol::TbankConfig;
992    ///
993    /// # #[tokio::main(flavor = "current_thread")]
994    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
995    /// # let server = MockServer::start(Default::default()).await;
996    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
997    /// let (events_tx, _events) = mpsc::channel(256);
998    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
999    /// ud.subscribe_order_state().await?;
1000    /// ud.subscribe_executions().await?;
1001    ///
1002    /// let report = ud.shutdown(ShutdownMode::Graceful { deadline: Duration::from_secs(2) }).await;
1003    /// assert_eq!(report.unsubscribed, 2, "two streams were open");
1004    /// assert!(report.inflight_abandoned.is_empty());
1005    /// # Ok(())
1006    /// # }
1007    /// ```
1008    pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport {
1009        let inner = &self.inner;
1010        let now = brokapi_runtime::now();
1011        inner
1012            .ctl
1013            .lock()
1014            .expect("ctl poisoned")
1015            .gate_mut()
1016            .begin_shutdown();
1017
1018        // In-flight placements are abandoned locally: Unknown, one event each. Exchange orders stay.
1019        let mut sink = EventSink::new();
1020        let abandoned = inner
1021            .core
1022            .lock()
1023            .expect("ud core poisoned")
1024            .abandon_inflight(now, &mut sink);
1025        inner.emitter.emit(sink.drain());
1026
1027        // Server-side streams have no unsubscribe message: ending the call is the protocol's way.
1028        let open: Vec<StreamKind> = inner
1029            .streams
1030            .lock()
1031            .expect("streams poisoned")
1032            .keys()
1033            .copied()
1034            .collect();
1035        let mut unsubscribed = 0;
1036        if let ShutdownMode::Graceful { deadline } = mode {
1037            for kind in &open {
1038                inner.stop_stream(*kind);
1039                unsubscribed += 1;
1040            }
1041            let end = brokapi_runtime::now() + deadline;
1042            while inner.emitter.has_backlog() && brokapi_runtime::now() < end {
1043                inner.emitter.flush();
1044                tokio::time::sleep(Duration::from_millis(5)).await;
1045            }
1046        }
1047        {
1048            let mut ctl = inner.ctl.lock().expect("ctl poisoned");
1049            let mut core = inner.core.lock().expect("ud core poisoned");
1050            ctl.shutdown_local(mode, now, core.orders_mut(), || (0, 0));
1051        }
1052        inner.cancel.cancel();
1053        let _ = self.pump.await;
1054        ShutdownReport::new(unsubscribed, 0, abandoned)
1055    }
1056
1057    fn check_command(&self) -> Result<(), Error> {
1058        let mut ctl = self.inner.ctl.lock().expect("ctl poisoned");
1059        if self.inner.emitter.is_degraded() {
1060            ctl.gate_mut().mark_degraded();
1061        }
1062        ctl.gate().check_command()
1063    }
1064
1065    fn acquire(&self, method: &str) -> Result<(), Error> {
1066        self.inner.limiter.acquire(method, brokapi_runtime::now())
1067    }
1068
1069    fn account(&self) -> String {
1070        self.inner.account.clone()
1071    }
1072
1073    /// Reads the active stop orders. The first request has no period. When the broker answers 30261 (too many
1074    /// stop orders for one reply), the period `from`..`to` is halved until every piece fits. The pieces are
1075    /// merged, a stop order that falls in two pieces is kept once. At most [`STOP_READ_MAX_REQUESTS`] requests.
1076    async fn read_active_stops(&self) -> Result<Vec<pb::StopOrder>, Error> {
1077        let mut found: Vec<pb::StopOrder> = Vec::new();
1078        let mut seen = std::collections::HashSet::new();
1079        // `None` is the whole list, `Some((from, to))` is a period in seconds.
1080        let mut todo: Vec<Option<(i64, i64)>> = vec![None];
1081        let mut requests = 0usize;
1082        while let Some(period) = todo.pop() {
1083            if requests == STOP_READ_MAX_REQUESTS {
1084                return Err(stop_list_too_long(requests));
1085            }
1086            requests += 1;
1087            self.acquire("GetStopOrders")?;
1088            let stamp = |secs: i64| prost_types::Timestamp {
1089                seconds: secs,
1090                nanos: 0,
1091            };
1092            let req = pb::GetStopOrdersRequest {
1093                account_id: self.account(),
1094                status: pb::StopOrderStatusOption::StopOrderStatusActive as i32,
1095                from: period.map(|(from, _)| stamp(from)),
1096                to: period.map(|(_, to)| stamp(to)),
1097            };
1098            let mut c = self.inner.client.stop_orders();
1099            match query(self.inner.request_timeout, c.get_stop_orders(req)).await {
1100                Ok(r) => {
1101                    for o in r.stop_orders {
1102                        if seen.insert(o.stop_order_id.clone()) {
1103                            found.push(o);
1104                        }
1105                    }
1106                }
1107                Err(Error::Rejected { code, .. }) if code == STOP_ORDERS_RESPONSE_LIMIT => {
1108                    let (from, to) = period.unwrap_or((STOP_READ_FROM, STOP_READ_TO));
1109                    if to - from < 2 {
1110                        return Err(stop_list_too_long(requests));
1111                    }
1112                    // Both halves share the middle second, a stop order there is merged by its number.
1113                    let mid = from + (to - from) / 2;
1114                    todo.push(Some((mid, to)));
1115                    todo.push(Some((from, mid)));
1116                }
1117                Err(e) => return Err(e),
1118            }
1119        }
1120        Ok(found)
1121    }
1122
1123    async fn subscribe(
1124        &self,
1125        kind: UdSubKind,
1126        opts: SubscribeOpts,
1127    ) -> Result<SubscriptionId, Error> {
1128        let inner = &self.inner;
1129        if let Some(id) = inner.core.lock().expect("ud core poisoned").sub(kind) {
1130            return Ok(id);
1131        }
1132        let id = inner.sub_ids.next();
1133        inner.emitter.register(id, None, opts.backpressure);
1134        let (tx, rx) = oneshot::channel();
1135        inner
1136            .waiters
1137            .lock()
1138            .expect("waiters poisoned")
1139            .insert(id, tx);
1140        inner
1141            .core
1142            .lock()
1143            .expect("ud core poisoned")
1144            .add_sub(id, kind);
1145        let streams: &[StreamKind] = match kind {
1146            UdSubKind::OrderState => &[StreamKind::OrderState],
1147            UdSubKind::Executions => &[StreamKind::Trades],
1148            UdSubKind::Portfolio => &[StreamKind::Portfolio, StreamKind::Positions],
1149        };
1150        for s in streams {
1151            inner.start_stream(*s);
1152        }
1153        let outcome = tokio::time::timeout(inner.request_timeout, rx).await;
1154        let failure = match outcome {
1155            Ok(Ok(Ok(()))) => return Ok(id),
1156            Ok(Ok(Err(reason))) => Error::Rejected {
1157                code: reason,
1158                message: "subscription refused".into(),
1159            },
1160            _ => transport(format!(
1161                "subscription not confirmed within {:?} ({})",
1162                inner.request_timeout,
1163                inner
1164                    .last_error
1165                    .get()
1166                    .unwrap_or_else(|| "no answer from the stream".into())
1167            )),
1168        };
1169        inner.waiters.lock().expect("waiters poisoned").remove(&id);
1170        inner
1171            .core
1172            .lock()
1173            .expect("ud core poisoned")
1174            .remove_sub(kind);
1175        inner.emitter.unregister(id);
1176        for s in streams {
1177            inner.stop_stream(*s);
1178        }
1179        Err(failure)
1180    }
1181
1182    /// Runs `f` on the core and delivers the events it produced.
1183    fn core<R>(&self, f: impl FnOnce(&mut UdCore, &mut EventSink) -> R) -> R {
1184        self.inner.with_core(f)
1185    }
1186
1187    async fn lot_of(&self, id: &InstrumentId) -> Result<i64, Error> {
1188        self.inner.lot_of(id).await
1189    }
1190
1191    /// The lot size of an instrument named by the fields of a message.
1192    async fn lot_of_parts(
1193        &self,
1194        uid: &str,
1195        figi: &str,
1196        ticker: &str,
1197        class_code: &str,
1198    ) -> Result<i64, Error> {
1199        match self.inner.lots.get_parts(uid, figi, ticker, class_code) {
1200            Some(lot) => Ok(lot),
1201            None => {
1202                self.lot_of(&id_from_parts(uid, figi, ticker, class_code))
1203                    .await
1204            }
1205        }
1206    }
1207
1208    /// Parts of an order message with the lot size of its instrument (one directory read the first time).
1209    async fn parts_of(&self, o: &pb::OrderState) -> Result<map_order::OrderParts, Error> {
1210        let id = id_from_parts(&o.instrument_uid, &o.figi, &o.ticker, &o.class_code);
1211        let lot =
1212            match self
1213                .inner
1214                .lots
1215                .get_parts(&o.instrument_uid, &o.figi, &o.ticker, &o.class_code)
1216            {
1217                Some(lot) => lot,
1218                None => self.lot_of(&id).await?,
1219            };
1220        Ok(map_order::parts_from_state(o).with_lot(lot))
1221    }
1222
1223    async fn get_state(&self, order_id: String, by_request: bool) -> Result<pb::OrderState, Error> {
1224        self.acquire("GetOrderState")?;
1225        let req = pb::GetOrderStateRequest {
1226            account_id: self.account(),
1227            order_id,
1228            price_type: 0,
1229            order_id_type: Some(if by_request {
1230                pb::OrderIdType::Request as i32
1231            } else {
1232                pb::OrderIdType::Exchange as i32
1233            }),
1234        };
1235        let mut c = self.inner.client.orders();
1236        match tokio::time::timeout(self.inner.request_timeout, c.get_order_state(req)).await {
1237            Ok(Ok(r)) => Ok(r.into_inner()),
1238            Ok(Err(s)) if is_not_found(&s) => {
1239                Err(Error::not_found("the broker does not know this order"))
1240            }
1241            Ok(Err(s)) => Err(map_status(&s)),
1242            Err(_) => Err(transport("request timed out")),
1243        }
1244    }
1245
1246    async fn get_active(&self) -> Result<Vec<OrderSnapshot>, Error> {
1247        self.acquire("GetOrders")?;
1248        let req = pb::GetOrdersRequest {
1249            account_id: self.account(),
1250            advanced_filters: None,
1251        };
1252        let mut c = self.inner.client.orders();
1253        let r = query(self.inner.request_timeout, c.get_orders(req)).await?;
1254        let now = brokapi_runtime::now();
1255        let mut all = Vec::with_capacity(r.orders.len());
1256        for o in &r.orders {
1257            all.push(self.parts_of(o).await?);
1258        }
1259        let mut core = self.inner.core.lock().expect("ud core poisoned");
1260        Ok(all
1261            .iter()
1262            .map(|parts| {
1263                core.note_order(parts);
1264                map_order::snapshot(parts, now)
1265            })
1266            .collect())
1267    }
1268}
1269
1270/// The token's only open account; several (or none) is a configuration error.
1271async fn pick_account(client: &TbankClient, timeout: Duration) -> Result<String, Error> {
1272    let mut c = client.users();
1273    let req = pb::GetAccountsRequest {
1274        status: Some(pb::AccountStatus::Open as i32),
1275    };
1276    let r = query(timeout, c.get_accounts(req)).await?;
1277    match r.accounts.as_slice() {
1278        [] => Err(config(
1279            "the token has no open account (a sandbox token needs an account opened with SandboxService/OpenSandboxAccount)",
1280        )),
1281        [one] => Ok(one.id.clone()),
1282        many => Err(config(format!(
1283            "the token sees {} accounts: set tbank.account_id (TbankClient::list_accounts shows the ids)",
1284            many.len()
1285        ))),
1286    }
1287}
1288
1289#[async_trait]
1290impl brokapi_core::Connector for UserDataConnector {
1291    type Caps = brokapi_core::UserDataCaps;
1292
1293    fn caps(&self) -> brokapi_core::UserDataCaps {
1294        UserDataConnector::caps(self)
1295    }
1296
1297    async fn shutdown(self, mode: brokapi_core::ShutdownMode) -> brokapi_core::ShutdownReport {
1298        UserDataConnector::shutdown(self, mode).await
1299    }
1300}
1301
1302#[async_trait]
1303impl OrderExecutor for UserDataConnector {
1304    /// Places an order (`PostOrder`). `req.cid` goes to the broker as `order_id`, its idempotency key.
1305    ///
1306    /// Units: `qty` is a whole number of LOTS (at least 1), `price` is per one instrument. The price is
1307    /// converted to units and nano. It is not rounded to the price step, the broker checks that.
1308    /// A limit order needs a positive price. A market order ignores the price. Commands on one
1309    /// instrument go one at a time, in the order they arrive.
1310    ///
1311    /// **Never repeated.** If the call gets no clear answer, the result is [`Error::Unknown`] and the
1312    /// library does not send it again, because a retry could create a second order. The `cid` is used up:
1313    /// placing it again is refused locally as `duplicate_place`. First ask
1314    /// [`order_state`](Self::order_state) or [`active_orders`](Self::active_orders), and place a
1315    /// new order under a NEW `cid` only when the broker has none.
1316    ///
1317    /// # Errors
1318    ///
1319    /// - [`Error::NotReconciled`] before the first successful [`reconcile`](Self::reconcile),
1320    ///   [`Error::Degraded`] after an event buffer overflow, [`Error::ShuttingDown`] during
1321    ///   shutdown. Nothing was sent.
1322    /// - [`Error::Rejected`] with code `invalid_qty` (zero, a fraction, or not a whole number of lots), `missing_price`
1323    ///   (limit order without a positive price) or `duplicate_place` (this `cid` was placed before).
1324    ///   Nothing was sent.
1325    /// - [`Error::Rejected`] when the broker refuses. The code is the gRPC status name (for example
1326    ///   `InvalidArgument`) and the message is the broker's error code. The same code `REJECTED` is
1327    ///   used when the broker accepted the call but reports the order as rejected. The broker did
1328    ///   nothing, and the `cid` stays used.
1329    /// - [`Error::RateLimited`] from the local limiter: refused before the `cid` is registered, so the
1330    ///   same `cid` can be used again. If the broker itself says `ResourceExhausted`, the `cid` is used up.
1331    /// - [`Error::Config`] when the broker does not accept the token (`Unauthenticated`).
1332    /// - [`Error::Unknown`] on a timeout (`request_timeout`), a broken connection or a server fault.
1333    ///   The order may or may not exist.
1334    ///
1335    /// # Examples
1336    ///
1337    /// ```
1338    /// # use brokapi_core::{Decimal, dec};
1339    /// use brokapi_core::{
1340    ///     ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
1341    ///     Price, Qty, ShutdownMode, Side,
1342    /// };
1343    /// use brokapi_runtime::BrokerLimiter;
1344    /// use brokapi_tbank::limits::default_limits;
1345    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1346    /// use tokio::sync::mpsc;
1347    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1348    /// # use brokapi_tbank_protocol::TbankConfig;
1349    ///
1350    /// # #[tokio::main(flavor = "current_thread")]
1351    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1352    /// # let server = MockServer::start(Default::default()).await;
1353    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1354    /// # let refused_price = brokapi_conformance::REJECT_PRICE;
1355    /// let (events_tx, _events) = mpsc::channel(256);
1356    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1357    /// ud.reconcile(&[]).await?;
1358    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1359    /// let order = |cid, qty, price| PlaceOrderRequest {
1360    ///     cid,
1361    ///     instrument: sber.clone(),
1362    ///     side: Side::Buy,
1363    ///     order_type: OrderType::Limit,
1364    ///     qty,
1365    ///     price: Some(price),
1366    /// };
1367    ///
1368    /// // The normal path.
1369    /// let cid = ClientOrderId::new_v4();
1370    /// let ack = ud.place(order(cid, Qty::from(10), Price::from(250))).await?;
1371    /// assert_eq!(ack.cid, cid);
1372    ///
1373    /// // The same cid again is refused locally, nothing is sent.
1374    /// match ud.place(order(cid, Qty::from(10), Price::from(250))).await {
1375    ///     Err(Error::Rejected { code, .. }) => assert_eq!(code, "duplicate_place"),
1376    ///     other => panic!("expected duplicate_place, got {other:?}"),
1377    /// }
1378    ///
1379    /// // Half a lot is refused before anything is sent.
1380    /// let half = ud.place(order(ClientOrderId::new_v4(), Qty::new(dec!(0.5)), Price::from(250))).await;
1381    /// assert!(matches!(half, Err(Error::Rejected { .. })));
1382    ///
1383    /// // The broker refuses a price (in the mock: 13.37). That is `Rejected`: it did nothing.
1384    /// let refused = ud.place(order(ClientOrderId::new_v4(), Qty::from(10), refused_price)).await;
1385    /// assert!(matches!(refused, Err(Error::Rejected { .. })));
1386    /// # ud.shutdown(ShutdownMode::Immediate).await;
1387    /// # Ok(())
1388    /// # }
1389    /// ```
1390    ///
1391    /// When the answer is lost, the result is `Unknown`. Here the mock answers 1.5 s late and the
1392    /// connector gives up after 200 ms. The order still reaches the exchange, so the right move is to
1393    /// look, not to place again.
1394    ///
1395    /// ```
1396    /// use std::sync::atomic::Ordering;
1397    /// use std::sync::Arc;
1398    /// use std::time::Duration;
1399    /// use brokapi_core::{
1400    ///     ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
1401    ///     Price, Qty, ShutdownMode, Side,
1402    /// };
1403    /// use brokapi_runtime::BrokerLimiter;
1404    /// use brokapi_tbank::limits::default_limits;
1405    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1406    /// use tokio::sync::mpsc;
1407    /// # use brokapi_mocks::tbank::{MockOptions, MockServer, ACCOUNT_A, TOKEN};
1408    /// # use brokapi_tbank_protocol::TbankConfig;
1409    ///
1410    /// # #[tokio::main(flavor = "current_thread")]
1411    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1412    /// # let opts = Arc::new(MockOptions::default());
1413    /// # opts.post_delay_ms.store(1500, Ordering::SeqCst);
1414    /// # let server = MockServer::start(opts).await;
1415    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint)
1416    /// #     .with_account(ACCOUNT_A)
1417    /// #     .with_request_timeout(Duration::from_millis(200));
1418    /// let (events_tx, _events) = mpsc::channel(256);
1419    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1420    /// ud.reconcile(&[]).await?;
1421    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1422    ///
1423    /// let cid = ClientOrderId::new_v4();
1424    /// let req = PlaceOrderRequest {
1425    ///     cid,
1426    ///     instrument: sber,
1427    ///     side: Side::Buy,
1428    ///     order_type: OrderType::Limit,
1429    ///     qty: Qty::from(10),
1430    ///     price: Some(Price::from(250)),
1431    /// };
1432    /// assert!(matches!(ud.place(req.clone()).await, Err(Error::Unknown)));
1433    ///
1434    /// // Do NOT place again. Ask the broker, with a bound on the waiting.
1435    /// let found = tokio::time::timeout(Duration::from_secs(10), async {
1436    ///     loop {
1437    ///         if let Ok(order) = ud.order_state(cid).await {
1438    ///             break order;
1439    ///         }
1440    ///         tokio::time::sleep(Duration::from_millis(100)).await;
1441    ///     }
1442    /// })
1443    /// .await?;
1444    /// assert_eq!(found.cid, cid);
1445    /// # ud.shutdown(ShutdownMode::Immediate).await;
1446    /// # Ok(())
1447    /// # }
1448    /// ```
1449    async fn place(&self, req: PlaceOrderRequest) -> Result<OrderAck, Error> {
1450        self.check_command()?;
1451        // Units to lots. A quantity that is not a whole number of lots is refused here, before the
1452        // quota and the cid are spent. The first order on an instrument reads its lot size.
1453        let lot = self.lot_of(&req.instrument).await?;
1454        let lots = pieces_to_lots(req.qty, lot)?;
1455        let price = match (req.order_type, req.price) {
1456            (OrderType::Limit, Some(p)) if p.is_positive() => {
1457                // Before a quota is spent or the cid is marked.
1458                self.inner.price_grid.check(&req.instrument.key, p)?;
1459                Some(price_to_quotation(p)?)
1460            }
1461            (OrderType::Limit, _) => {
1462                return Err(Error::Rejected {
1463                    code: "missing_price".into(),
1464                    message: "a limit order needs a positive price".into(),
1465                });
1466            }
1467            (OrderType::Market, _) => None,
1468        };
1469        self.acquire("PostOrder")?;
1470        // Waits for earlier commands on this instrument, released on every exit path.
1471        let _instrument = self.inner.locks.lock(&req.instrument).await;
1472        // The gate may have closed while this command stood in the queue (degraded, shutdown): ask again
1473        // before the cid is marked and anything is sent.
1474        self.check_command()?;
1475
1476        let now = brokapi_runtime::now();
1477        self.core(|c, _| c.begin_place(&req, now))?;
1478
1479        let wire = pb::PostOrderRequest {
1480            quantity: lots,
1481            price,
1482            direction: map_order::direction(req.side) as i32,
1483            account_id: self.account(),
1484            order_type: map_order::wire_order_type(req.order_type) as i32,
1485            order_id: req.cid.hyphenated().to_string(),
1486            instrument_id: instrument_ref(&req.instrument),
1487            ..Default::default()
1488        };
1489        let cid = req.cid;
1490        let inner = Arc::clone(&self.inner);
1491        // From here the cid is marked and the order is in flight. The request, the answer and the update
1492        // of the order run as a task of their own: if the caller drops this future (a timeout around it, a
1493        // lost `select!`), the task still ends and the order does not stay in flight for ever.
1494        let work = async move {
1495            let _instrument = _instrument;
1496            let mut c = inner.client.orders();
1497            let outcome = command(inner.request_timeout, c.post_order(wire)).await;
1498            match outcome {
1499                CommandOutcome::Done(resp) => {
1500                    inner.with_core(|c, s| c.place_response(cid, &resp, now, s));
1501                    if resp.execution_report_status()
1502                        == pb::OrderExecutionReportStatus::ExecutionReportStatusRejected
1503                    {
1504                        return Err(Error::Rejected {
1505                            code: "REJECTED".into(),
1506                            message: resp.message,
1507                        });
1508                    }
1509                    Ok(OrderAck {
1510                        cid,
1511                        broker_id: BrokerOrderId::new(resp.order_id),
1512                        ts: stamps(now, None),
1513                    })
1514                }
1515                CommandOutcome::Refused(status, err) => {
1516                    let reason = format!("{:?}: {}", status.code(), status.message());
1517                    inner.with_core(|c, s| c.place_rejected(cid, reason, now, s));
1518                    Err(err)
1519                }
1520                CommandOutcome::Unknown => {
1521                    inner.with_core(|c, s| c.place_unknown(cid, now, s));
1522                    Err(Error::Unknown)
1523                }
1524            }
1525        };
1526        match brokapi_runtime::run_detached(work).await {
1527            Some(result) => result,
1528            // The task was torn down with the runtime: the order may exist.
1529            None => Err(Error::Unknown),
1530        }
1531    }
1532
1533    /// Cancels an order by the `cid` you placed it with (`CancelOrder` by request id).
1534    ///
1535    /// `Ok` means the broker accepted the cancel. The final state arrives on the order stream, or ask
1536    /// [`order_state`](Self::order_state). The exchange order id is not needed. Commands on the same
1537    /// instrument go one at a time. The library does not send a cancel twice on its own.
1538    ///
1539    /// # Errors
1540    ///
1541    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is
1542    ///   closed, nothing was sent.
1543    /// - [`Error::Rejected`] when the broker refuses. The usual reason is code `not_found`: the broker
1544    ///   has no such order (never placed, or already done).
1545    /// - [`Error::RateLimited`] when a quota is used up.
1546    /// - [`Error::Unknown`] on a timeout or a broken connection: the order may be cancelled or still
1547    ///   working. Check [`order_state`](Self::order_state).
1548    ///
1549    /// # Examples
1550    ///
1551    /// ```
1552    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1553    /// use brokapi_runtime::BrokerLimiter;
1554    /// use brokapi_tbank::limits::default_limits;
1555    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1556    /// use tokio::sync::mpsc;
1557    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1558    /// # use brokapi_tbank_protocol::TbankConfig;
1559    ///
1560    /// # #[tokio::main(flavor = "current_thread")]
1561    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1562    /// # let server = MockServer::start(Default::default()).await;
1563    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1564    /// let (events_tx, mut events) = mpsc::channel(256);
1565    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1566    /// ud.reconcile(&[]).await?;
1567    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1568    /// let buy = |cid, price| PlaceOrderRequest {
1569    ///     cid,
1570    ///     instrument: sber.clone(),
1571    ///     side: Side::Buy,
1572    ///     order_type: OrderType::Limit,
1573    ///     qty: Qty::from(10),
1574    ///     price: Some(price),
1575    /// };
1576    ///
1577    /// let cid = ClientOrderId::new_v4();
1578    /// ud.place(buy(cid, Price::from(250))).await?;
1579    /// ud.cancel(cid).await?;
1580    ///
1581    /// // A cid the broker has never seen is a refusal.
1582    /// assert!(matches!(ud.cancel(ClientOrderId::new_v4()).await, Err(Error::Rejected { .. })));
1583    /// # ud.shutdown(ShutdownMode::Immediate).await;
1584    /// # Ok(())
1585    /// # }
1586    /// ```
1587    async fn cancel(&self, cid: ClientOrderId) -> Result<(), Error> {
1588        self.check_command()?;
1589        self.acquire("CancelOrder")?;
1590        // The instrument comes from the connector's order registry; an unknown order is not serialized.
1591        let instrument = self.core(|c, _| c.instrument_of(cid));
1592        let _instrument = match &instrument {
1593            Some(i) => Some(self.inner.locks.lock(i).await),
1594            None => None,
1595        };
1596        // The gate may have closed while this command stood in the queue.
1597        self.check_command()?;
1598        let wire = pb::CancelOrderRequest {
1599            account_id: self.account(),
1600            order_id: cid.hyphenated().to_string(),
1601            order_id_type: Some(pb::OrderIdType::Request as i32),
1602        };
1603        let mut c = self.inner.client.orders();
1604        match command(self.inner.request_timeout, c.cancel_order(wire)).await {
1605            CommandOutcome::Done(_) => {
1606                let now = brokapi_runtime::now();
1607                self.core(|c, s| c.cancel_ok(cid, now, s));
1608                Ok(())
1609            }
1610            CommandOutcome::Refused(_, err) => Err(err),
1611            CommandOutcome::Unknown => Err(Error::Unknown),
1612        }
1613    }
1614
1615    /// `ReplaceOrder` needs the exchange order id and the quantity: both come from what the connector
1616    /// already knows about the original, else from one `GetOrderState`. `req.cid` is the new
1617    /// idempotency key, it replaces the old one.
1618    ///
1619    /// `req.qty` of `None` keeps the original quantity and `req.price` of `None` keeps the original
1620    /// price. A new quantity is a whole number of lots (in units, like in `place`). The old order is replaced by a new one,
1621    /// which gets `req.cid` and a new exchange id. Like `place`, the call is never repeated: a lost
1622    /// answer is [`Error::Unknown`], and `req.cid` is used up. Look at both orders before you try
1623    /// again.
1624    ///
1625    /// # Errors
1626    ///
1627    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: nothing was sent.
1628    /// - [`Error::Rejected`] with code `invalid_qty` or `duplicate_place` (the new `cid` was used
1629    ///   before), both before the replace is sent. [`Error::Rejected`] with code `not_found` when the
1630    ///   broker does not know the original order. Any other refusal of the broker, with the gRPC
1631    ///   status name as code.
1632    /// - [`Error::RateLimited`], [`Error::Transport`] and [`Error::Config`] while the connector
1633    ///   reads the original order (one `GetOrderState`, only when it does not know the order).
1634    /// - [`Error::Unknown`] on a timeout or a broken connection during the replace itself.
1635    ///
1636    /// # Examples
1637    ///
1638    /// ```
1639    /// use brokapi_core::ReplaceOrderRequest;
1640    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1641    /// use brokapi_runtime::BrokerLimiter;
1642    /// use brokapi_tbank::limits::default_limits;
1643    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1644    /// use tokio::sync::mpsc;
1645    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1646    /// # use brokapi_tbank_protocol::TbankConfig;
1647    ///
1648    /// # #[tokio::main(flavor = "current_thread")]
1649    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1650    /// # let server = MockServer::start(Default::default()).await;
1651    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1652    /// let (events_tx, mut events) = mpsc::channel(256);
1653    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1654    /// ud.reconcile(&[]).await?;
1655    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1656    /// let buy = |cid, price| PlaceOrderRequest {
1657    ///     cid,
1658    ///     instrument: sber.clone(),
1659    ///     side: Side::Buy,
1660    ///     order_type: OrderType::Limit,
1661    ///     qty: Qty::from(10),
1662    ///     price: Some(price),
1663    /// };
1664    ///
1665    /// let first = ClientOrderId::new_v4();
1666    /// ud.place(buy(first, Price::from(250))).await?;
1667    ///
1668    /// // Same quantity, new price. The new order has a new cid.
1669    /// let second = ClientOrderId::new_v4();
1670    /// let ack = ud
1671    ///     .replace(ReplaceOrderRequest { cid: second, original_cid: first, qty: None, price: Some(Price::from(251)) })
1672    ///     .await?;
1673    /// assert_eq!(ack.cid, second);
1674    ///
1675    /// // An order the broker has never heard of cannot be replaced.
1676    /// let ghost = ReplaceOrderRequest { cid: ClientOrderId::new_v4(), original_cid: ClientOrderId::new_v4(), qty: None, price: None };
1677    /// assert!(matches!(ud.replace(ghost).await, Err(Error::Rejected { .. })));
1678    /// # ud.shutdown(ShutdownMode::Immediate).await;
1679    /// # Ok(())
1680    /// # }
1681    /// ```
1682    async fn replace(&self, req: ReplaceOrderRequest) -> Result<OrderAck, Error> {
1683        self.check_command()?;
1684        let original = req.original_cid;
1685        // The request has no instrument: it is the original order's. Not in the registry - one
1686        // GetOrderState teaches it (before the lock: the exchange id is read only under the lock,
1687        // a replace queued before this one changes it).
1688        let mut instrument = self.core(|c, _| c.instrument_of(original));
1689        if instrument.is_none() {
1690            let st = self
1691                .get_state(original.hyphenated().to_string(), true)
1692                .await?;
1693            let parts = self.parts_of(&st).await?;
1694            self.core(|c, _| c.note_order(&parts));
1695            instrument = self.core(|c, _| c.instrument_of(original));
1696        }
1697        let instrument =
1698            instrument.ok_or_else(|| Error::not_found("the original order is not known"))?;
1699        // Input is checked before anything is sent or any quota is spent: units to lots.
1700        let lot = self.lot_of(&instrument).await?;
1701        let requested_lots = req.qty.map(|q| pieces_to_lots(q, lot)).transpose()?;
1702        if let Some(p) = req.price {
1703            self.inner.price_grid.check(&instrument.key, p)?;
1704        }
1705        self.acquire("ReplaceOrder")?;
1706        let _instrument = Some(self.inner.locks.lock(&instrument).await);
1707        // The gate may have closed while this command stood in the queue.
1708        self.check_command()?;
1709        let known_id = self.core(|c, _| c.exchange_id(original).map(str::to_string));
1710        let known_basis = self.core(|c, _| c.order_basis(original));
1711        let (exchange_id, base_qty, base_price) = match (known_id, known_basis) {
1712            (Some(id), Some((q, p))) => (id, q, p),
1713            _ => {
1714                let st = self
1715                    .get_state(original.hyphenated().to_string(), true)
1716                    .await?;
1717                let parts = self.parts_of(&st).await?;
1718                self.core(|c, _| c.note_order(&parts));
1719                (st.order_id, parts.qty_requested(), parts.unit_price)
1720            }
1721        };
1722        let lots = match requested_lots {
1723            Some(lots) => lots,
1724            None => pieces_to_lots(base_qty, lot)?,
1725        };
1726        let price = req
1727            .price
1728            .or(base_price)
1729            .map(price_to_quotation)
1730            .transpose()?;
1731
1732        let now = brokapi_runtime::now();
1733        self.core(|c, _| c.begin_replace(req.cid, original, now))?;
1734        let wire = pb::ReplaceOrderRequest {
1735            account_id: self.account(),
1736            order_id: exchange_id,
1737            idempotency_key: req.cid.hyphenated().to_string(),
1738            quantity: lots,
1739            price,
1740            price_type: None,
1741            confirm_margin_trade: false,
1742        };
1743        let mut c = self.inner.client.orders();
1744        let cid = req.cid;
1745        match command(self.inner.request_timeout, c.replace_order(wire)).await {
1746            CommandOutcome::Done(resp) => {
1747                self.core(|c, s| c.replace_response(cid, &resp, now, s));
1748                Ok(OrderAck {
1749                    cid,
1750                    broker_id: BrokerOrderId::new(resp.order_id),
1751                    ts: stamps(now, None),
1752                })
1753            }
1754            CommandOutcome::Refused(status, err) => {
1755                let reason = format!("{:?}: {}", status.code(), status.message());
1756                self.core(|c, s| c.replace_failed(cid, false, reason, now, s));
1757                Err(err)
1758            }
1759            CommandOutcome::Unknown => {
1760                self.core(|c, s| c.replace_failed(cid, true, String::new(), now, s));
1761                Err(Error::Unknown)
1762            }
1763        }
1764    }
1765
1766    /// The current state of one order by its `cid` (`GetOrderState` by request id).
1767    ///
1768    /// It also works for an order that is already filled or cancelled, so it is the way to learn the
1769    /// final state. `qty` and `filled` are in units of the instrument. The gate is not required: you may call it before
1770    /// the first reconcile.
1771    ///
1772    /// # Errors
1773    ///
1774    /// - [`Error::Rejected`] with code `not_found` when the broker does not know the order.
1775    /// - [`Error::RateLimited`] when a quota is used up. Repeat after `retry_after`.
1776    /// - [`Error::Transport`] on a timeout or a network failure. A read, so repeat it later.
1777    /// - [`Error::Config`] when the broker does not accept the token.
1778    ///
1779    /// # Examples
1780    ///
1781    /// ```
1782    /// use brokapi_core::OrderState;
1783    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1784    /// use brokapi_runtime::BrokerLimiter;
1785    /// use brokapi_tbank::limits::default_limits;
1786    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1787    /// use tokio::sync::mpsc;
1788    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1789    /// # use brokapi_tbank_protocol::TbankConfig;
1790    ///
1791    /// # #[tokio::main(flavor = "current_thread")]
1792    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1793    /// # let server = MockServer::start(Default::default()).await;
1794    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1795    /// let (events_tx, mut events) = mpsc::channel(256);
1796    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1797    /// ud.reconcile(&[]).await?;
1798    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1799    /// let buy = |cid, price| PlaceOrderRequest {
1800    ///     cid,
1801    ///     instrument: sber.clone(),
1802    ///     side: Side::Buy,
1803    ///     order_type: OrderType::Limit,
1804    ///     qty: Qty::from(10),
1805    ///     price: Some(price),
1806    /// };
1807    ///
1808    /// let cid = ClientOrderId::new_v4();
1809    /// ud.place(buy(cid, Price::from(250))).await?;
1810    /// let snapshot = ud.order_state(cid).await?;
1811    /// assert_eq!(snapshot.cid, cid);
1812    /// assert!(matches!(snapshot.state, OrderState::Acked { .. }));
1813    ///
1814    /// assert!(matches!(ud.order_state(ClientOrderId::new_v4()).await, Err(Error::Rejected { .. })));
1815    /// # ud.shutdown(ShutdownMode::Immediate).await;
1816    /// # Ok(())
1817    /// # }
1818    /// ```
1819    async fn order_state(&self, cid: ClientOrderId) -> Result<OrderSnapshot, Error> {
1820        let st = self.get_state(cid.hyphenated().to_string(), true).await?;
1821        let parts = self.parts_of(&st).await?;
1822        self.core(|c, _| c.note_order(&parts));
1823        Ok(map_order::snapshot(&parts, brokapi_runtime::now()))
1824    }
1825
1826    /// Orders that are live at the broker now (`GetOrders`) for this account.
1827    ///
1828    /// Orders that ended (filled, cancelled, rejected) are not listed. An order placed elsewhere
1829    /// (phone, another program) is listed too, with a stand-in `cid` made from its exchange id.
1830    /// `qty` and `filled` are in units of the instrument.
1831    ///
1832    /// # Errors
1833    ///
1834    /// [`Error::RateLimited`], [`Error::Transport`], [`Error::Config`] and [`Error::Rejected`], as for
1835    /// [`order_state`](Self::order_state). A read, so repeat it later.
1836    ///
1837    /// # Examples
1838    ///
1839    /// ```
1840    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1841    /// use brokapi_runtime::BrokerLimiter;
1842    /// use brokapi_tbank::limits::default_limits;
1843    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1844    /// use tokio::sync::mpsc;
1845    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1846    /// # use brokapi_tbank_protocol::TbankConfig;
1847    ///
1848    /// # #[tokio::main(flavor = "current_thread")]
1849    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1850    /// # let server = MockServer::start(Default::default()).await;
1851    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1852    /// let (events_tx, mut events) = mpsc::channel(256);
1853    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1854    /// ud.reconcile(&[]).await?;
1855    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1856    /// let buy = |cid, price| PlaceOrderRequest {
1857    ///     cid,
1858    ///     instrument: sber.clone(),
1859    ///     side: Side::Buy,
1860    ///     order_type: OrderType::Limit,
1861    ///     qty: Qty::from(10),
1862    ///     price: Some(price),
1863    /// };
1864    ///
1865    /// assert!(ud.active_orders().await?.is_empty());
1866    /// let cid = ClientOrderId::new_v4();
1867    /// ud.place(buy(cid, Price::from(250))).await?;
1868    /// let live = ud.active_orders().await?;
1869    /// assert_eq!(live.iter().map(|o| o.cid).collect::<Vec<_>>(), vec![cid]);
1870    /// # ud.shutdown(ShutdownMode::Immediate).await;
1871    /// # Ok(())
1872    /// # }
1873    /// ```
1874    async fn active_orders(&self) -> Result<Vec<OrderSnapshot>, Error> {
1875        self.get_active().await
1876    }
1877
1878    /// Streams the state changes of your orders (`OrderStateStream`), Lossless.
1879    ///
1880    /// The buffer holds `order_state_buffer` events. Own orders are never dropped: on overflow an
1881    /// overflow event is sent and the trading gate closes until the next successful
1882    /// [`reconcile`](Self::reconcile). Calling it again returns the same subscription id. The stream
1883    /// reconnects by itself. After a reconnect the connector reads `GetOrders` and emits what
1884    /// changed. An order that ended during the gap is not in `GetOrders`: find it with
1885    /// [`order_state`](Self::order_state).
1886    ///
1887    /// # Errors
1888    ///
1889    /// - [`Error::Rejected`] when the broker refuses the stream.
1890    /// - [`Error::Transport`] when the broker did not confirm the stream within `request_timeout`.
1891    ///
1892    /// # Examples
1893    ///
1894    /// ```
1895    /// use std::time::Duration;
1896    /// use brokapi_core::{Event, OrderState};
1897    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1898    /// use brokapi_runtime::BrokerLimiter;
1899    /// use brokapi_tbank::limits::default_limits;
1900    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1901    /// use tokio::sync::mpsc;
1902    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1903    /// # use brokapi_tbank_protocol::TbankConfig;
1904    ///
1905    /// # #[tokio::main(flavor = "current_thread")]
1906    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1907    /// # let server = MockServer::start(Default::default()).await;
1908    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1909    /// let (events_tx, mut events) = mpsc::channel(256);
1910    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1911    /// ud.reconcile(&[]).await?;
1912    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1913    /// let buy = |cid, price| PlaceOrderRequest {
1914    ///     cid,
1915    ///     instrument: sber.clone(),
1916    ///     side: Side::Buy,
1917    ///     order_type: OrderType::Limit,
1918    ///     qty: Qty::from(10),
1919    ///     price: Some(price),
1920    /// };
1921    ///
1922    /// let sub = ud.subscribe_order_state().await?;
1923    /// assert_eq!(ud.subscribe_order_state().await?, sub, "the same id again");
1924    ///
1925    /// let cid = ClientOrderId::new_v4();
1926    /// ud.place(buy(cid, Price::from(250))).await?;
1927    /// let state = tokio::time::timeout(Duration::from_secs(5), async {
1928    ///     loop {
1929    ///         let env = events.recv().await.expect("the channel stays open");
1930    ///         if let Event::Order(snapshot) = env.payload {
1931    ///             if snapshot.cid == cid {
1932    ///                 break snapshot.state;
1933    ///             }
1934    ///         }
1935    ///     }
1936    /// })
1937    /// .await?;
1938    /// assert!(matches!(state, OrderState::Acked { .. } | OrderState::InFlight { .. }));
1939    /// # ud.shutdown(ShutdownMode::Immediate).await;
1940    /// # Ok(())
1941    /// # }
1942    /// ```
1943    async fn subscribe_order_state(&self) -> Result<SubscriptionId, Error> {
1944        // Own order states are never dropped: Lossless only.
1945        let opts = SubscribeOpts {
1946            backpressure: Backpressure::Lossless {
1947                cap: self.inner.order_state_buffer,
1948            },
1949            depth: None,
1950        };
1951        self.subscribe(UdSubKind::OrderState, opts).await
1952    }
1953
1954    /// One `GetOrders` call: the live orders at the broker are compared with the ledger. A cid in
1955    /// the ledger that is not live is `closed_offline` (filled, cancelled, rejected, or never
1956    /// accepted - ask `order_state` for which). Live orders without our cid are `foreign`.
1957    /// A successful reconcile opens the command gate and ends `Degraded`.
1958    ///
1959    /// `ledger` is the list of cids you know to be yours (an empty slice on a fresh start). Until
1960    /// this call succeeds once, every trading command is refused with [`Error::NotReconciled`].
1961    ///
1962    /// # Errors
1963    ///
1964    /// [`Error::RateLimited`], [`Error::Transport`], [`Error::Config`] and [`Error::Rejected`], as for
1965    /// [`active_orders`](Self::active_orders). The gate stays closed when it fails. Repeat the call.
1966    ///
1967    /// # Examples
1968    ///
1969    /// ```
1970    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
1971    /// use brokapi_runtime::BrokerLimiter;
1972    /// use brokapi_tbank::limits::default_limits;
1973    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
1974    /// use tokio::sync::mpsc;
1975    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
1976    /// # use brokapi_tbank_protocol::TbankConfig;
1977    ///
1978    /// # #[tokio::main(flavor = "current_thread")]
1979    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1980    /// # let server = MockServer::start(Default::default()).await;
1981    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
1982    /// let (events_tx, mut events) = mpsc::channel(256);
1983    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
1984    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1985    /// let buy = |cid, price| PlaceOrderRequest {
1986    ///     cid,
1987    ///     instrument: sber.clone(),
1988    ///     side: Side::Buy,
1989    ///     order_type: OrderType::Limit,
1990    ///     qty: Qty::from(10),
1991    ///     price: Some(price),
1992    /// };
1993    ///
1994    /// // A run places an order, then the program restarts and knows only the cid from its own record.
1995    /// ud.reconcile(&[]).await?;
1996    /// let cid = ClientOrderId::new_v4();
1997    /// ud.place(buy(cid, Price::from(250))).await?;
1998    ///
1999    /// let lost = ClientOrderId::new_v4();
2000    /// let report = ud.reconcile(&[cid, lost]).await?;
2001    /// assert_eq!(report.confirmed.iter().map(|o| o.cid).collect::<Vec<_>>(), vec![cid]);
2002    /// // A cid that is not live at the broker: filled, cancelled or never accepted. Ask `order_state`.
2003    /// assert_eq!(report.closed_offline, vec![lost]);
2004    /// assert!(report.foreign.is_empty());
2005    /// # ud.shutdown(ShutdownMode::Immediate).await;
2006    /// # Ok(())
2007    /// # }
2008    /// ```
2009    async fn reconcile(&self, ledger: &[ClientOrderId]) -> Result<ReconcileReport, Error> {
2010        let active = self.get_active().await?;
2011        let ledger_snaps: Vec<OrderSnapshot> = active
2012            .iter()
2013            .filter(|o| ledger.contains(&o.cid))
2014            .cloned()
2015            .collect();
2016        let mut report = reconcile_ledger(&ledger_snaps, &active);
2017        for cid in ledger {
2018            if !active.iter().any(|o| o.cid == *cid) && !report.closed_offline.contains(cid) {
2019                report.closed_offline.push(*cid);
2020            }
2021        }
2022        self.inner.emitter.clear_degraded();
2023        let mut ctl = self.inner.ctl.lock().expect("ctl poisoned");
2024        ctl.gate_mut().clear_degraded();
2025        ctl.gate_mut().mark_reconciled();
2026        Ok(report)
2027    }
2028}
2029
2030#[async_trait]
2031impl ExecutionSource for UserDataConnector {
2032    /// History from `GetOperationsByCursor` (executed BUY/SELL operations, one `Execution` per trade).
2033    /// Differences from the live stream are in `map::execution::from_operation`.
2034    ///
2035    /// Quantities here are in units of the instrument, like everywhere (the broker reports the history and the stream of own trades in pieces, not in lots).
2036    /// `cid` is `None`: the history has no order id. The commission of an operation is spread over its
2037    /// trades in proportion to quantity. At most 20 pages of 1000 operations are read per call. A
2038    /// range that holds more is an error, never a cut-off list: ask for shorter ranges.
2039    ///
2040    /// # Errors
2041    ///
2042    /// - [`Error::Rejected`] with code `range_too_long` when the range holds more than 20 pages of
2043    ///   1000 operations. Nothing is returned, so a short list never passes for the whole history.
2044    ///   Split the range and read the parts.
2045    /// - [`Error::RateLimited`] (the operations quota is small), [`Error::Transport`], [`Error::Config`]
2046    ///   and [`Error::Rejected`], as for [`order_state`](OrderExecutor::order_state). A read, so repeat
2047    ///   it later.
2048    ///
2049    /// # Examples
2050    ///
2051    /// ```
2052    /// use brokapi_core::{ExecutionSource, TimeRange, UnixNanos};
2053    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2054    /// use brokapi_runtime::BrokerLimiter;
2055    /// use brokapi_tbank::limits::default_limits;
2056    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2057    /// use tokio::sync::mpsc;
2058    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2059    /// # use brokapi_tbank_protocol::TbankConfig;
2060    ///
2061    /// # #[tokio::main(flavor = "current_thread")]
2062    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2063    /// # let server = MockServer::start(Default::default()).await;
2064    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2065    /// let (events_tx, mut events) = mpsc::channel(256);
2066    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2067    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2068    ///
2069    /// let range = TimeRange::new(UnixNanos::ZERO, UnixNanos::from_nanos(i64::MAX / 2));
2070    /// let history = ud.executions(range).await?;
2071    /// assert!(history.iter().all(|x| x.qty.is_positive()));
2072    /// # ud.shutdown(ShutdownMode::Immediate).await;
2073    /// # Ok(())
2074    /// # }
2075    /// ```
2076    async fn executions(&self, range: TimeRange) -> Result<Vec<Execution>, Error> {
2077        let mut out = Vec::new();
2078        let mut cursor: Option<String> = None;
2079        for _ in 0..MAX_HISTORY_PAGES {
2080            self.acquire("GetOperationsByCursor")?;
2081            let req = pb::GetOperationsByCursorRequest {
2082                account_id: self.account(),
2083                from: Some(nanos_to_proto(range.from)),
2084                to: Some(nanos_to_proto(range.to)),
2085                cursor: cursor.clone(),
2086                limit: Some(1000),
2087                // Margin-call trades and trades paid by card are trades of the account as well.
2088                operation_types: vec![
2089                    pb::OperationType::Buy as i32,
2090                    pb::OperationType::Sell as i32,
2091                    pb::OperationType::BuyMargin as i32,
2092                    pb::OperationType::SellMargin as i32,
2093                    pb::OperationType::BuyCard as i32,
2094                    pb::OperationType::SellCard as i32,
2095                ],
2096                state: Some(pb::OperationState::Executed as i32),
2097                without_trades: Some(false),
2098                ..Default::default()
2099            };
2100            let mut c = self.inner.client.operations();
2101            let r = query(self.inner.request_timeout, c.get_operations_by_cursor(req)).await?;
2102            let now = brokapi_runtime::now();
2103            for item in &r.items {
2104                out.extend(execution::from_operation(item, now));
2105            }
2106            if !r.has_next || r.next_cursor.is_empty() {
2107                return Ok(out);
2108            }
2109            cursor = Some(r.next_cursor);
2110        }
2111        Err(Error::Rejected {
2112            code: "range_too_long".into(),
2113            message: format!(
2114                "the range holds more than {} operations; read it in shorter ranges",
2115                MAX_HISTORY_PAGES * 1000
2116            ),
2117        })
2118    }
2119
2120    /// Own trades as they happen (`TradesStream`), Lossless.
2121    ///
2122    /// Same buffer and overflow rules as [`subscribe_order_state`](OrderExecutor::subscribe_order_state).
2123    /// Quantities are in units of the instrument (the broker reports own trades in pieces) and the stream carries no commission (0.0). Calling it again returns the
2124    /// same id.
2125    ///
2126    /// # Errors
2127    ///
2128    /// The same as [`subscribe_order_state`](OrderExecutor::subscribe_order_state).
2129    ///
2130    /// # Examples
2131    ///
2132    /// ```
2133    /// use brokapi_core::ExecutionSource;
2134    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2135    /// use brokapi_runtime::BrokerLimiter;
2136    /// use brokapi_tbank::limits::default_limits;
2137    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2138    /// use tokio::sync::mpsc;
2139    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2140    /// # use brokapi_tbank_protocol::TbankConfig;
2141    ///
2142    /// # #[tokio::main(flavor = "current_thread")]
2143    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2144    /// # let server = MockServer::start(Default::default()).await;
2145    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2146    /// let (events_tx, mut events) = mpsc::channel(256);
2147    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2148    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2149    ///
2150    /// let sub = ud.subscribe_executions().await?;
2151    /// assert_eq!(ud.subscribe_executions().await?, sub);
2152    /// # ud.shutdown(ShutdownMode::Immediate).await;
2153    /// # Ok(())
2154    /// # }
2155    /// ```
2156    async fn subscribe_executions(&self) -> Result<SubscriptionId, Error> {
2157        let opts = SubscribeOpts {
2158            backpressure: Backpressure::Lossless {
2159                cap: self.inner.order_state_buffer,
2160            },
2161            depth: None,
2162        };
2163        self.subscribe(UdSubKind::Executions, opts).await
2164    }
2165}
2166
2167impl UserDataConnector {
2168    async fn fetch_positions(&self) -> Result<pb::PositionsResponse, Error> {
2169        self.acquire("GetPositions")?;
2170        let mut c = self.inner.client.operations();
2171        query(
2172            self.inner.request_timeout,
2173            c.get_positions(pb::PositionsRequest {
2174                account_id: self.account(),
2175            }),
2176        )
2177        .await
2178    }
2179
2180    async fn fetch_portfolio(&self) -> Result<pb::PortfolioResponse, Error> {
2181        self.acquire("GetPortfolio")?;
2182        let mut c = self.inner.client.operations();
2183        query(
2184            self.inner.request_timeout,
2185            c.get_portfolio(pb::PortfolioRequest {
2186                account_id: self.account(),
2187                currency: None,
2188            }),
2189        )
2190        .await
2191    }
2192}
2193
2194#[async_trait]
2195impl PortfolioSource for UserDataConnector {
2196    /// Cash from `GetPositions`, positions from `GetPortfolio` (two calls).
2197    ///
2198    /// The two calls are not one atomic snapshot: the account may change between them. Cash lines are
2199    /// per currency (upper case), with `available` and `blocked`. Currency holdings are not repeated
2200    /// in `positions`.
2201    ///
2202    /// # Errors
2203    ///
2204    /// [`Error::RateLimited`], [`Error::Transport`], [`Error::Config`] and [`Error::Rejected`], as for
2205    /// [`order_state`](OrderExecutor::order_state). A read, so repeat it later.
2206    ///
2207    /// # Examples
2208    ///
2209    /// ```
2210    /// use brokapi_core::PortfolioSource;
2211    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2212    /// use brokapi_runtime::BrokerLimiter;
2213    /// use brokapi_tbank::limits::default_limits;
2214    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2215    /// use tokio::sync::mpsc;
2216    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2217    /// # use brokapi_tbank_protocol::TbankConfig;
2218    ///
2219    /// # #[tokio::main(flavor = "current_thread")]
2220    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2221    /// # let server = MockServer::start(Default::default()).await;
2222    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2223    /// let (events_tx, mut events) = mpsc::channel(256);
2224    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2225    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2226    ///
2227    /// let portfolio = ud.portfolio().await?;
2228    /// assert!(!portfolio.balances.is_empty());
2229    /// assert!(!portfolio.positions.is_empty());
2230    /// # ud.shutdown(ShutdownMode::Immediate).await;
2231    /// # Ok(())
2232    /// # }
2233    /// ```
2234    async fn portfolio(&self) -> Result<Portfolio, Error> {
2235        let positions = self.fetch_positions().await?;
2236        let portfolio = self.fetch_portfolio().await?;
2237        let now = brokapi_runtime::now();
2238        let cash = map_portfolio::balances_from_positions(&positions, now);
2239        Ok(map_portfolio::portfolio(&portfolio, cash, now))
2240    }
2241
2242    /// Quantity in units of the instrument, prices in the settlement currency.
2243    ///
2244    /// One `GetPortfolio` call. Currency holdings are left out (see
2245    /// [`balances`](PortfolioSource::balances)).
2246    ///
2247    /// # Errors
2248    ///
2249    /// The same as [`portfolio`](PortfolioSource::portfolio).
2250    ///
2251    /// # Examples
2252    ///
2253    /// ```
2254    /// use brokapi_core::PortfolioSource;
2255    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2256    /// use brokapi_runtime::BrokerLimiter;
2257    /// use brokapi_tbank::limits::default_limits;
2258    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2259    /// use tokio::sync::mpsc;
2260    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2261    /// # use brokapi_tbank_protocol::TbankConfig;
2262    ///
2263    /// # #[tokio::main(flavor = "current_thread")]
2264    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2265    /// # let server = MockServer::start(Default::default()).await;
2266    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2267    /// let (events_tx, mut events) = mpsc::channel(256);
2268    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2269    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2270    ///
2271    /// let positions = ud.positions().await?;
2272    /// assert!(positions.iter().all(|p| !p.qty.is_negative()));
2273    /// # ud.shutdown(ShutdownMode::Immediate).await;
2274    /// # Ok(())
2275    /// # }
2276    /// ```
2277    async fn positions(&self) -> Result<Vec<Position>, Error> {
2278        let p = self.fetch_portfolio().await?;
2279        Ok(map_portfolio::positions_from_portfolio(
2280            &p,
2281            brokapi_runtime::now(),
2282        ))
2283    }
2284
2285    /// Cash by currency from `GetPositions`: `available` money and the `blocked` part.
2286    ///
2287    /// Currency codes are upper case. One call.
2288    ///
2289    /// # Errors
2290    ///
2291    /// The same as [`portfolio`](PortfolioSource::portfolio).
2292    ///
2293    /// # Examples
2294    ///
2295    /// ```
2296    /// # use brokapi_core::{Money};
2297    /// use brokapi_core::PortfolioSource;
2298    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2299    /// use brokapi_runtime::BrokerLimiter;
2300    /// use brokapi_tbank::limits::default_limits;
2301    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2302    /// use tokio::sync::mpsc;
2303    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2304    /// # use brokapi_tbank_protocol::TbankConfig;
2305    ///
2306    /// # #[tokio::main(flavor = "current_thread")]
2307    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2308    /// # let server = MockServer::start(Default::default()).await;
2309    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2310    /// let (events_tx, mut events) = mpsc::channel(256);
2311    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2312    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2313    ///
2314    /// let cash = ud.balances().await?;
2315    /// assert!(cash.iter().all(|b| b.available >= Money::from(0)));
2316    /// # ud.shutdown(ShutdownMode::Immediate).await;
2317    /// # Ok(())
2318    /// # }
2319    /// ```
2320    async fn balances(&self) -> Result<Vec<Balance>, Error> {
2321        let p = self.fetch_positions().await?;
2322        Ok(map_portfolio::balances_from_positions(
2323            &p,
2324            brokapi_runtime::now(),
2325        ))
2326    }
2327
2328    /// `PortfolioStream` (positions with prices) and `PositionsStream` (cash and balances) under one id.
2329    ///
2330    /// Two server streams feed one subscription. The events are [`Event::Portfolio`] (a full portfolio
2331    /// from `PortfolioStream`, with prices, and the latest known cash), [`Event::Balances`] (cash
2332    /// changed) and [`Event::Positions`] (positions changed, no prices). Calling it again returns the
2333    /// same id (the options of the first call stay).
2334    ///
2335    /// [`Event::Portfolio`]: brokapi_core::Event::Portfolio
2336    /// [`Event::Balances`]: brokapi_core::Event::Balances
2337    /// [`Event::Positions`]: brokapi_core::Event::Positions
2338    ///
2339    /// # Errors
2340    ///
2341    /// The same as [`subscribe_order_state`](OrderExecutor::subscribe_order_state).
2342    ///
2343    /// # Examples
2344    ///
2345    /// ```
2346    /// use std::time::Duration;
2347    /// use brokapi_core::{Event, PortfolioSource, SubscribeOpts};
2348    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
2349    /// use brokapi_runtime::BrokerLimiter;
2350    /// use brokapi_tbank::limits::default_limits;
2351    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2352    /// use tokio::sync::mpsc;
2353    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2354    /// # use brokapi_tbank_protocol::TbankConfig;
2355    ///
2356    /// # #[tokio::main(flavor = "current_thread")]
2357    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2358    /// # let server = MockServer::start(Default::default()).await;
2359    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2360    /// let (events_tx, mut events) = mpsc::channel(256);
2361    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2362    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2363    ///
2364    /// let sub = ud.subscribe_portfolio(SubscribeOpts::conflate()).await?;
2365    /// let first = tokio::time::timeout(Duration::from_secs(5), async {
2366    ///     loop {
2367    ///         let env = events.recv().await.expect("the channel stays open");
2368    ///         if let Event::Portfolio(p) = env.payload {
2369    ///             break (env.sub, p);
2370    ///         }
2371    ///     }
2372    /// })
2373    /// .await?;
2374    /// assert_eq!(first.0, Some(sub));
2375    /// # ud.shutdown(ShutdownMode::Immediate).await;
2376    /// # Ok(())
2377    /// # }
2378    /// ```
2379    async fn subscribe_portfolio(&self, opts: SubscribeOpts) -> Result<SubscriptionId, Error> {
2380        self.subscribe(UdSubKind::Portfolio, opts).await
2381    }
2382}
2383
2384#[async_trait]
2385impl StopOrderExecutor for UserDataConnector {
2386    /// `StopOrdersService/PostStopOrder`. `cid` goes to `order_id` (the idempotency key), as for
2387    /// plain orders. The broker's stop order does not carry it back, so the link cid <-> broker id is
2388    /// kept in memory: after a restart `active_stop_orders` shows `cid: None` and a stop order is
2389    /// cancelled by `StopOrderRef::Broker`.
2390    ///
2391    /// **After [`Error::Unknown`] the link is made later.** The connector keeps what was asked for
2392    /// (instrument, side, kind, lots, stop price, limit price) and the time the request was in flight.
2393    /// The next [`active_stop_orders`](Self::active_stop_orders) that finds exactly one stop order with
2394    /// no cid of ours that fits this request, and was created within that time (give or take 10 seconds for
2395    /// the difference of the clocks), shows it with this `cid`, and from then on the stop order can be
2396    /// cancelled by it. If two stop orders fit, or two lost placements are equal, nothing is linked
2397    /// and the stop order shows `cid: None`: the connector does not guess. Read the list after the
2398    /// answer was lost, and do not place again under another cid while a stop order with `cid: None` of the
2399    /// same instrument and side is there. This rests on the creation time that T-Bank puts in the list.
2400    /// Sending the same request again with the same `order_id` is not used: whether T-Bank treats
2401    /// `order_id` of a stop order as an idempotency key is not verified live.
2402    ///
2403    /// Units: `qty` is a whole number of LOTS (at least 1). Prices are positive. A stop-limit order
2404    /// needs a limit price. Expiry is `GoodTillCancel` or `GoodTillDate`: T-Bank stop orders have no
2405    /// end-of-day expiry. The checks run before any quota is spent. Like `place`, the call is never
2406    /// repeated: a lost answer is [`Error::Unknown`], and so is an answer with no stop order id
2407    /// (the broker may hold the order, nobody can tell). In both cases the `cid` is used up.
2408    ///
2409    /// # Errors
2410    ///
2411    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: nothing was sent.
2412    /// - [`Error::Rejected`] with code `invalid_qty`, `invalid_price`, `missing_limit_price` or
2413    ///   `unsupported_expiry` for a bad request, or `duplicate_place` for a `cid` used before.
2414    ///   Nothing was sent. A refusal by the broker is [`Error::Rejected`] with the gRPC status name
2415    ///   as code. After a refusal by the broker the `cid` may be used again.
2416    /// - [`Error::RateLimited`] when a quota is used up. Nothing was sent.
2417    /// - [`Error::Unknown`] on a timeout, a broken connection, a server fault or an answer without an
2418    ///   id. Check [`active_stop_orders`](Self::active_stop_orders) before you place again.
2419    ///
2420    /// # Examples
2421    ///
2422    /// ```
2423    /// use brokapi_core::{
2424    ///     OrderExecutor, PlaceStopOrderRequest, Price, Qty, StopExpiry, StopOrderExecutor, StopOrderKind, StopOrderRef,
2425    /// };
2426    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
2427    /// use brokapi_runtime::BrokerLimiter;
2428    /// use brokapi_tbank::limits::default_limits;
2429    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2430    /// use tokio::sync::mpsc;
2431    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2432    /// # use brokapi_tbank_protocol::TbankConfig;
2433    ///
2434    /// # #[tokio::main(flavor = "current_thread")]
2435    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2436    /// # let server = MockServer::start(Default::default()).await;
2437    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2438    /// let (events_tx, mut _events) = mpsc::channel(256);
2439    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2440    /// ud.reconcile(&[]).await?;
2441    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2442    ///
2443    /// let cid = ClientOrderId::new_v4();
2444    /// let stop_loss = PlaceStopOrderRequest {
2445    ///     cid,
2446    ///     instrument: sber.clone(),
2447    ///     side: Side::Sell,
2448    ///     kind: StopOrderKind::StopLoss,
2449    ///     qty: Qty::from(10),
2450    ///     stop_price: Price::from(200),
2451    ///     limit_price: None, // market order at the trigger
2452    ///     expiry: StopExpiry::GoodTillCancel,
2453    /// };
2454    /// let ack = ud.place_stop(stop_loss).await?;
2455    /// assert_eq!(ud.active_stop_orders().await?[0].cid, Some(cid));
2456    ///
2457    /// // A stop-limit order without a limit price is refused before anything is sent.
2458    /// let bad = PlaceStopOrderRequest {
2459    ///     cid: ClientOrderId::new_v4(),
2460    ///     instrument: sber,
2461    ///     side: Side::Sell,
2462    ///     kind: StopOrderKind::StopLimit,
2463    ///     qty: Qty::from(10),
2464    ///     stop_price: Price::from(200),
2465    ///     limit_price: None,
2466    ///     expiry: StopExpiry::GoodTillCancel,
2467    /// };
2468    /// assert!(matches!(ud.place_stop(bad).await, Err(Error::Rejected { .. })));
2469    ///
2470    /// ud.cancel_stop(StopOrderRef::Broker(ack.broker_id)).await?;
2471    /// assert!(ud.active_stop_orders().await?.is_empty());
2472    /// # ud.shutdown(ShutdownMode::Immediate).await;
2473    /// # Ok(())
2474    /// # }
2475    /// ```
2476    async fn place_stop(&self, req: PlaceStopOrderRequest) -> Result<OrderAck, Error> {
2477        self.check_command()?;
2478        let lot = self.lot_of(&req.instrument).await?;
2479        let lots = pieces_to_lots(req.qty, lot)?;
2480        let wire = map_stop::build_post(&self.account(), &req, lots)?;
2481        self.inner
2482            .price_grid
2483            .check(&req.instrument.key, req.stop_price)?;
2484        if let Some(p) = req.limit_price {
2485            self.inner.price_grid.check(&req.instrument.key, p)?;
2486        }
2487        self.acquire("PostStopOrder")?;
2488        let _instrument = self.inner.locks.lock(&req.instrument).await;
2489        // The gate may have closed while this command stood in the queue.
2490        self.check_command()?;
2491        if !self
2492            .inner
2493            .stops
2494            .lock()
2495            .expect("stops poisoned")
2496            .begin(req.cid)
2497        {
2498            return Err(Error::Rejected {
2499                code: "duplicate_place".into(),
2500                message: "auto-retry of place_stop is forbidden".into(),
2501            });
2502        }
2503        let now = brokapi_runtime::now();
2504        let sent = wall_nanos().as_nanos();
2505        let mut c = self.inner.client.stop_orders();
2506        let cid = req.cid;
2507        match command(self.inner.request_timeout, c.post_stop_order(wire)).await {
2508            CommandOutcome::Done(resp) if !resp.stop_order_id.is_empty() => {
2509                self.inner
2510                    .stops
2511                    .lock()
2512                    .expect("stops poisoned")
2513                    .bind(cid, &resp.stop_order_id);
2514                Ok(OrderAck {
2515                    cid,
2516                    broker_id: BrokerOrderId::new(resp.stop_order_id),
2517                    ts: stamps(now, None),
2518                })
2519            }
2520            // No clear answer: the broker may hold the stop order (a lost answer, or an answer with no id).
2521            // Keep what was asked for, so that the stop order can be linked to this cid when it shows up
2522            // in the list.
2523            CommandOutcome::Done(_) | CommandOutcome::Unknown => {
2524                self.inner.stops.lock().expect("stops poisoned").lose(
2525                    req,
2526                    lots,
2527                    sent,
2528                    wall_nanos().as_nanos(),
2529                );
2530                Err(Error::Unknown)
2531            }
2532            CommandOutcome::Refused(_, err) => {
2533                self.inner
2534                    .stops
2535                    .lock()
2536                    .expect("stops poisoned")
2537                    .forget_cid(cid);
2538                Err(err)
2539            }
2540        }
2541    }
2542
2543    /// Cancels a stop order by its `cid` or by the broker's id (`CancelStopOrder`).
2544    ///
2545    /// A `cid` works only for a stop order placed by this connector object: the link lives in
2546    /// memory and is lost on restart. After a restart cancel by `StopOrderRef::Broker`, taking the
2547    /// id from [`active_stop_orders`](Self::active_stop_orders). Not repeated automatically.
2548    ///
2549    /// # Errors
2550    ///
2551    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: nothing was sent.
2552    /// - [`Error::Rejected`] with code `unknown_stop_order` when the `cid` is not known to this
2553    ///   connector. The broker's `NotFound` is `not_found`, other refusals come with the gRPC status name as code.
2554    /// - [`Error::RateLimited`] when a quota is used up.
2555    /// - [`Error::Unknown`] on a timeout or a broken connection: the stop order may or may not be
2556    ///   cancelled. Check [`active_stop_orders`](Self::active_stop_orders).
2557    ///
2558    /// # Examples
2559    ///
2560    /// ```
2561    /// use brokapi_core::{OrderExecutor, StopOrderExecutor, StopOrderRef};
2562    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
2563    /// use brokapi_runtime::BrokerLimiter;
2564    /// use brokapi_tbank::limits::default_limits;
2565    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2566    /// use tokio::sync::mpsc;
2567    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2568    /// # use brokapi_tbank_protocol::TbankConfig;
2569    ///
2570    /// # #[tokio::main(flavor = "current_thread")]
2571    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2572    /// # let server = MockServer::start(Default::default()).await;
2573    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2574    /// let (events_tx, mut _events) = mpsc::channel(256);
2575    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2576    /// ud.reconcile(&[]).await?;
2577    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2578    ///
2579    /// // A cid this connector never placed cannot be mapped to a broker id.
2580    /// let result = ud.cancel_stop(StopOrderRef::Cid(ClientOrderId::new_v4())).await;
2581    /// match result {
2582    ///     Err(Error::Rejected { code, .. }) => assert_eq!(code, "unknown_stop_order"),
2583    ///     other => panic!("expected unknown_stop_order, got {other:?}"),
2584    /// }
2585    /// # ud.shutdown(ShutdownMode::Immediate).await;
2586    /// # Ok(())
2587    /// # }
2588    /// ```
2589    async fn cancel_stop(&self, stop: StopOrderRef) -> Result<(), Error> {
2590        self.check_command()?;
2591        let id = match stop {
2592            StopOrderRef::Broker(id) => id.as_str().to_string(),
2593            StopOrderRef::Cid(cid) => self.inner.stops.lock().expect("stops poisoned").id_of(cid).ok_or_else(|| {
2594                Error::Rejected {
2595                    code: "unknown_stop_order".into(),
2596                    message: format!(
2597                        "stop order {cid} was not placed by this connector instance (T-Bank does not return the cid): cancel by the broker id"
2598                    ),
2599                }
2600            })?,
2601        };
2602        self.acquire("CancelStopOrder")?;
2603        let wire = pb::CancelStopOrderRequest {
2604            account_id: self.account(),
2605            stop_order_id: id.clone(),
2606        };
2607        let mut c = self.inner.client.stop_orders();
2608        match command(self.inner.request_timeout, c.cancel_stop_order(wire)).await {
2609            CommandOutcome::Done(_) => {
2610                self.inner
2611                    .stops
2612                    .lock()
2613                    .expect("stops poisoned")
2614                    .forget_id(&id);
2615                Ok(())
2616            }
2617            CommandOutcome::Refused(_, err) => Err(err),
2618            CommandOutcome::Unknown => Err(Error::Unknown),
2619        }
2620    }
2621
2622    /// The stop orders that are active now (`GetStopOrders` with the active filter).
2623    ///
2624    /// `cid` is filled only for stop orders placed by this connector object. Others (placed earlier,
2625    /// by hand, by another program) have `cid: None`: use their `broker_id`. A stop order whose
2626    /// placement ended in [`Error::Unknown`] gets its `cid` here, when it can be told for sure, see
2627    /// [`place_stop`](Self::place_stop).
2628    ///
2629    /// # Errors
2630    ///
2631    /// [`Error::RateLimited`], [`Error::Transport`], [`Error::Config`] and [`Error::Rejected`], as for
2632    /// [`order_state`](OrderExecutor::order_state). A read, so repeat it later.
2633    ///
2634    /// The broker refuses to answer with too many stop orders at once (error 30261, "narrow the request with
2635    /// `from` and `to`"). The connector first asks for the whole list. On 30261 it halves the period until every
2636    /// piece fits and merges the pieces, up to 40 requests. If that is still not enough, the error is
2637    /// [`Error::Rejected`] with the code `stop_orders_response_limit`. Which time of a stop order `from` and `to`
2638    /// compare with, and how many stop orders one answer may hold, are not documented and not verified live.
2639    ///
2640    /// # Examples
2641    ///
2642    /// ```
2643    /// use brokapi_core::{OrderExecutor, StopOrderExecutor};
2644    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
2645    /// use brokapi_runtime::BrokerLimiter;
2646    /// use brokapi_tbank::limits::default_limits;
2647    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2648    /// use tokio::sync::mpsc;
2649    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2650    /// # use brokapi_tbank_protocol::TbankConfig;
2651    ///
2652    /// # #[tokio::main(flavor = "current_thread")]
2653    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2654    /// # let server = MockServer::start(Default::default()).await;
2655    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2656    /// let (events_tx, mut _events) = mpsc::channel(256);
2657    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2658    /// ud.reconcile(&[]).await?;
2659    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2660    ///
2661    /// assert!(ud.active_stop_orders().await?.is_empty());
2662    /// # ud.shutdown(ShutdownMode::Immediate).await;
2663    /// # Ok(())
2664    /// # }
2665    /// ```
2666    async fn active_stop_orders(&self) -> Result<Vec<StopOrderSnapshot>, Error> {
2667        let raw = self.read_active_stops().await?;
2668        let now = brokapi_runtime::now();
2669        let mut lots = Vec::with_capacity(raw.len());
2670        for o in &raw {
2671            lots.push(
2672                self.lot_of_parts(&o.instrument_uid, &o.figi, &o.ticker, &o.class_code)
2673                    .await?,
2674            );
2675        }
2676        let mut stops = self.inner.stops.lock().expect("stops poisoned");
2677        stops.adopt(&raw, wall_nanos().as_nanos());
2678        Ok(raw
2679            .iter()
2680            .zip(lots)
2681            .map(|(o, lot)| map_stop::snapshot(o, stops.cid_of(&o.stop_order_id), now, lot))
2682            .collect())
2683    }
2684
2685    /// T-Bank has no stream of stop order statuses (`StopOrdersService` is unary only).
2686    ///
2687    /// # Errors
2688    ///
2689    /// Always [`Error::Unsupported`] (`stop_order_stream`), without a network call. The library does
2690    /// not fake the stream by polling. Poll [`active_stop_orders`](Self::active_stop_orders) yourself.
2691    ///
2692    /// # Examples
2693    ///
2694    /// ```
2695    /// use brokapi_core::{OrderExecutor, StopOrderExecutor};
2696    /// use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
2697    /// use brokapi_runtime::BrokerLimiter;
2698    /// use brokapi_tbank::limits::default_limits;
2699    /// use brokapi_tbank::{UserDataConfig, UserDataConnector};
2700    /// use tokio::sync::mpsc;
2701    /// # use brokapi_mocks::tbank::{MockServer, ACCOUNT_A, TOKEN};
2702    /// # use brokapi_tbank_protocol::TbankConfig;
2703    ///
2704    /// # #[tokio::main(flavor = "current_thread")]
2705    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2706    /// # let server = MockServer::start(Default::default()).await;
2707    /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint).with_account(ACCOUNT_A);
2708    /// let (events_tx, mut _events) = mpsc::channel(256);
2709    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
2710    /// ud.reconcile(&[]).await?;
2711    /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
2712    ///
2713    /// assert!(matches!(ud.subscribe_stop_orders().await, Err(Error::Unsupported { .. })));
2714    /// # ud.shutdown(ShutdownMode::Immediate).await;
2715    /// # Ok(())
2716    /// # }
2717    /// ```
2718    async fn subscribe_stop_orders(&self) -> Result<SubscriptionId, Error> {
2719        Err(Error::Unsupported {
2720            capability: "stop_order_stream".into(),
2721        })
2722    }
2723}
2724
2725#[cfg(test)]
2726mod stop_book_tests {
2727    use super::*;
2728
2729    fn cid(n: u8) -> ClientOrderId {
2730        ClientOrderId::from_bytes([n; 16])
2731    }
2732
2733    const SEC: i64 = 1_000_000_000;
2734    /// A moment of the wall clock, in nanoseconds.
2735    const T0: i64 = 1_800_000_000 * SEC;
2736
2737    fn request(n: u8) -> PlaceStopOrderRequest {
2738        use brokapi_core::{InstrumentKey, NativeId, Price, Qty, Side, StopExpiry, StopOrderKind};
2739        PlaceStopOrderRequest {
2740            cid: cid(n),
2741            instrument: InstrumentId::new(
2742                InstrumentKey::new("SBER", "TQBR"),
2743                NativeId::Uid("uid-sber".into()),
2744            ),
2745            side: Side::Sell,
2746            kind: StopOrderKind::StopLoss,
2747            qty: Qty::from(10),
2748            stop_price: Price::from(200),
2749            limit_price: None,
2750            expiry: StopExpiry::GoodTillCancel,
2751        }
2752    }
2753
2754    /// The stop order the broker holds for `request`, created at `created` (seconds).
2755    fn broker_stop(id: &str, created: i64) -> pb::StopOrder {
2756        pb::StopOrder {
2757            stop_order_id: id.into(),
2758            lots_requested: 1,
2759            direction: pb::StopOrderDirection::Sell as i32,
2760            order_type: pb::StopOrderType::StopLoss as i32,
2761            create_date: Some(prost_types::Timestamp {
2762                seconds: created,
2763                nanos: 0,
2764            }),
2765            stop_price: Some(pb::MoneyValue {
2766                currency: "rub".into(),
2767                units: 200,
2768                nano: 0,
2769            }),
2770            instrument_uid: "uid-sber".into(),
2771            ticker: "SBER".into(),
2772            class_code: "TQBR".into(),
2773            status: pb::StopOrderStatusOption::StopOrderStatusActive as i32,
2774            ..Default::default()
2775        }
2776    }
2777
2778    /// A lost placement of `request(n)`: sent at `T0`, given up one second later.
2779    fn lost_book(ns: &[u8]) -> StopBook {
2780        let mut book = StopBook::default();
2781        for &n in ns {
2782            assert!(book.begin(cid(n)));
2783            book.lose(request(n), 1, T0, T0 + SEC);
2784        }
2785        book
2786    }
2787
2788    #[test]
2789    fn a_lost_placement_is_linked_to_the_only_stop_order_that_fits() {
2790        let mut book = lost_book(&[1]);
2791        book.adopt(&[broker_stop("S-1", 1_800_000_000)], T0 + 2 * SEC);
2792        assert_eq!(book.cid_of("S-1"), Some(cid(1)));
2793        assert_eq!(book.id_of(cid(1)).as_deref(), Some("S-1"));
2794        assert!(book.lost.is_empty(), "linked: no longer waiting");
2795    }
2796
2797    #[test]
2798    fn the_creation_time_must_lie_in_the_window_of_the_request() {
2799        // Sent at T0, given up at T0 + 1 s, 10 s of margin on both sides.
2800        for (created, linked) in [
2801            (1_800_000_000 - 11, false),
2802            (1_800_000_000 - 9, true),
2803            (1_800_000_000 + 10, true),
2804            (1_800_000_000 + 12, false),
2805        ] {
2806            let mut book = lost_book(&[1]);
2807            book.adopt(&[broker_stop("S-1", created)], T0 + 2 * SEC);
2808            assert_eq!(book.cid_of("S-1").is_some(), linked, "created {created}");
2809        }
2810    }
2811
2812    #[test]
2813    fn what_was_asked_for_must_match() {
2814        let mut other_price = broker_stop("S-1", 1_800_000_000);
2815        other_price.stop_price.as_mut().unwrap().units = 201;
2816        let mut other_side = broker_stop("S-1", 1_800_000_000);
2817        other_side.direction = pb::StopOrderDirection::Buy as i32;
2818        let mut other_lots = broker_stop("S-1", 1_800_000_000);
2819        other_lots.lots_requested = 2;
2820        let mut other_instrument = broker_stop("S-1", 1_800_000_000);
2821        other_instrument.instrument_uid = "uid-gazp".into();
2822        other_instrument.ticker = "GAZP".into();
2823        let mut with_limit = broker_stop("S-1", 1_800_000_000);
2824        with_limit.order_type = pb::StopOrderType::StopLimit as i32;
2825        with_limit.price = Some(pb::MoneyValue {
2826            currency: "rub".into(),
2827            units: 199,
2828            nano: 0,
2829        });
2830        for o in [
2831            other_price,
2832            other_side,
2833            other_lots,
2834            other_instrument,
2835            with_limit,
2836        ] {
2837            let mut book = lost_book(&[1]);
2838            book.adopt(&[o], T0 + 2 * SEC);
2839            assert_eq!(book.cid_of("S-1"), None);
2840        }
2841    }
2842
2843    #[test]
2844    fn two_equal_stop_orders_for_one_placement_link_nothing() {
2845        // One of them may be placed by hand: the connector does not guess which is ours.
2846        let mut book = lost_book(&[1]);
2847        book.adopt(
2848            &[
2849                broker_stop("S-1", 1_800_000_000),
2850                broker_stop("S-2", 1_800_000_001),
2851            ],
2852            T0 + 2 * SEC,
2853        );
2854        assert_eq!((book.cid_of("S-1"), book.cid_of("S-2")), (None, None));
2855        assert_eq!(book.lost.len(), 1, "still waiting: the window is open");
2856    }
2857
2858    #[test]
2859    fn two_equal_placements_link_nothing_even_for_two_stop_orders() {
2860        let mut book = lost_book(&[1, 2]);
2861        book.adopt(
2862            &[
2863                broker_stop("S-1", 1_800_000_000),
2864                broker_stop("S-2", 1_800_000_001),
2865            ],
2866            T0 + 2 * SEC,
2867        );
2868        assert_eq!((book.cid_of("S-1"), book.cid_of("S-2")), (None, None));
2869        // And one stop order for two equal placements: it is not ours more than the other's.
2870        let mut book = lost_book(&[1, 2]);
2871        book.adopt(&[broker_stop("S-1", 1_800_000_000)], T0 + 2 * SEC);
2872        assert_eq!(book.cid_of("S-1"), None);
2873    }
2874
2875    #[test]
2876    fn different_placements_are_linked_each_to_its_own_stop_order() {
2877        let mut book = lost_book(&[1]);
2878        let mut second = request(2);
2879        second.stop_price = brokapi_core::Price::from(150);
2880        assert!(book.begin(cid(2)));
2881        book.lose(second, 1, T0, T0 + SEC);
2882        let mut s2 = broker_stop("S-2", 1_800_000_000);
2883        s2.stop_price.as_mut().unwrap().units = 150;
2884        book.adopt(&[s2, broker_stop("S-1", 1_800_000_000)], T0 + 2 * SEC);
2885        assert_eq!(book.cid_of("S-1"), Some(cid(1)));
2886        assert_eq!(book.cid_of("S-2"), Some(cid(2)));
2887    }
2888
2889    #[test]
2890    fn a_stop_order_that_already_has_a_cid_is_not_taken() {
2891        let mut book = lost_book(&[1]);
2892        assert!(book.begin(cid(9)));
2893        book.bind(cid(9), "S-1");
2894        book.adopt(&[broker_stop("S-1", 1_800_000_000)], T0 + 2 * SEC);
2895        assert_eq!(book.cid_of("S-1"), Some(cid(9)), "the first owner keeps it");
2896        assert_eq!(book.id_of(cid(1)), None);
2897    }
2898
2899    #[test]
2900    fn a_placement_is_forgotten_after_a_read_that_came_after_its_window() {
2901        let mut book = lost_book(&[1]);
2902        // Within the window nothing is found yet: the placement waits (the broker may be slow).
2903        book.adopt(&[], T0 + 5 * SEC);
2904        assert_eq!(book.lost.len(), 1);
2905        // A read after the window (given up at T0 + 1 s, 10 s of margin) finds nothing: forgotten.
2906        book.adopt(&[], T0 + 12 * SEC);
2907        assert!(book.lost.is_empty());
2908        // A stop order that shows up now is not linked to a forgotten placement.
2909        book.adopt(&[broker_stop("S-1", 1_800_000_000)], T0 + 13 * SEC);
2910        assert_eq!(book.cid_of("S-1"), None);
2911    }
2912
2913    #[test]
2914    fn the_waiting_placements_are_bounded() {
2915        let mut book = StopBook::default();
2916        for n in 0..(LOST_STOP_CAP + 5) {
2917            book.lose(request(1), n as i64, T0, T0);
2918        }
2919        assert_eq!(book.lost.len(), LOST_STOP_CAP);
2920        assert_eq!(book.lost[0].lots, 5, "the oldest went first");
2921    }
2922
2923    #[test]
2924    fn a_repeated_cid_does_not_touch_the_record_of_the_first_send() {
2925        let mut book = StopBook::default();
2926        assert!(book.begin(cid(1)));
2927        book.bind(cid(1), "S-1");
2928        // The same cid again is refused, and the broker's id that was learned is still there.
2929        assert!(!book.begin(cid(1)));
2930        assert_eq!(book.id_of(cid(1)).as_deref(), Some("S-1"));
2931        assert_eq!(book.cid_of("S-1"), Some(cid(1)));
2932    }
2933
2934    #[test]
2935    fn a_cancelled_stop_keeps_its_cid_used() {
2936        let mut book = StopBook::default();
2937        assert!(book.begin(cid(1)));
2938        book.bind(cid(1), "S-1");
2939        book.forget_id("S-1");
2940        // The broker id no longer maps to the cid, but the cid cannot be sent again.
2941        assert_eq!(book.cid_of("S-1"), None);
2942        assert!(!book.begin(cid(1)));
2943    }
2944
2945    #[test]
2946    fn a_stop_the_broker_refused_for_sure_can_be_sent_again() {
2947        let mut book = StopBook::default();
2948        assert!(book.begin(cid(1)));
2949        book.forget_cid(cid(1));
2950        assert!(book.begin(cid(1)));
2951        assert_eq!(book.id_of(cid(1)), None, "no broker id yet");
2952    }
2953
2954    #[test]
2955    fn the_book_is_bounded_and_keeps_the_live_stops() {
2956        let mut book = StopBook::default();
2957        // One live stop, then more than the window of finished ones.
2958        assert!(book.begin(cid(0)));
2959        book.bind(cid(0), "LIVE");
2960        for n in 0..(STOP_BOOK_CAP + 50) {
2961            let mut bytes = [1u8; 16];
2962            bytes[..8].copy_from_slice(&(n as u64).to_be_bytes());
2963            assert!(book.begin(ClientOrderId::from_bytes(bytes)));
2964        }
2965        assert!(
2966            book.by_cid.len() <= STOP_BOOK_CAP + 1,
2967            "{}",
2968            book.by_cid.len()
2969        );
2970        // The live stop is still known, the oldest finished ones are forgotten.
2971        assert_eq!(book.id_of(cid(0)).as_deref(), Some("LIVE"));
2972        let mut first = [1u8; 16];
2973        first[..8].copy_from_slice(&0u64.to_be_bytes());
2974        assert!(
2975            book.begin(ClientOrderId::from_bytes(first)),
2976            "past the window the cid is free again"
2977        );
2978    }
2979}