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(¶ms)`) 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}