brokapi_tbank/md.rs
1//! T-Bank market data connector
2//!
3//! [`MarketDataConnector`] is the market data side of the T-Bank connector. It reads order books,
4//! trades, quotes, candles and the instrument directory. It is not tied to an account.
5
6use std::collections::HashMap;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use async_trait::async_trait;
12use brokapi_core::{
13 Candle, Envelope, Error, Instrument, InstrumentId, InstrumentKind, InstrumentQuery,
14 InstrumentSource, MarketDataCaps, MarketDataSource, MarketTrade, OrderBook, OrderBookSource,
15 Schedule, ShutdownMode, ShutdownReport, StreamingCaps, SubscribeOpts, SubscriptionId,
16 TimeRange, Timeframe, TradingParams, TradingStatus, UnixNanos,
17};
18use brokapi_runtime::BrokerLimiter;
19use brokapi_sansio::{
20 BrokerCore, DesiredSubscription, EventSink, SubscriptionKind, SubscriptionSet,
21};
22use brokapi_tbank_protocol::TbankClient;
23use brokapi_tbank_protocol::v1 as pb;
24use tokio::sync::{Notify, mpsc, oneshot};
25use tokio::task::JoinHandle;
26use tokio_stream::wrappers::UnboundedReceiverStream;
27use tokio_util::sync::CancellationToken;
28use tonic::Request;
29use tracing::warn;
30
31use crate::config::{MD_MAX_PING, MarketDataConfig, effective_ping, silence_limit};
32use crate::core::md::{MdCore, MdFrame};
33use crate::emit::Emitter;
34use crate::error::{config, transport};
35use crate::ids::{instrument_ref, instrument_request, nanos_to_proto, require_native, stamps};
36use crate::lots::{LotBook, event_instrument, scale_book, scale_candle, scale_event, scale_trade};
37use crate::map::{instrument as map_instrument, market, schedule as map_schedule};
38use crate::raw::TbankRaw;
39use crate::stream::{LastError, backoff, sleep_or_cancel};
40use crate::util::{SubIdGen, query};
41
42const DEFAULT_DEPTH: u32 = 20;
43
44struct Shared {
45 client: TbankClient,
46 limiter: Arc<BrokerLimiter>,
47 emitter: Arc<Emitter>,
48 core: Mutex<MdCore>,
49 /// Lot sizes of the instruments met: the broker counts book levels, trades and volumes in lots, the
50 /// events count in units.
51 lots: LotBook,
52 waiters: Mutex<HashMap<SubscriptionId, oneshot::Sender<Result<(), String>>>>,
53 subs: Mutex<Vec<DesiredSubscription>>,
54 sub_ids: SubIdGen,
55 notify: Notify,
56 last_error: Arc<LastError>,
57 request_timeout: Duration,
58 stale_after: Duration,
59 closing: AtomicBool,
60 cancel: CancellationToken,
61}
62
63impl Shared {
64 /// Runs `f` on the core, then delivers what it produced (events, subscription results).
65 fn with_core<R>(&self, f: impl FnOnce(&mut MdCore, &mut EventSink) -> R) -> R {
66 let mut sink = EventSink::new();
67 let r = {
68 let mut core = self.core.lock().expect("md core poisoned");
69 let r = f(&mut core, &mut sink);
70 for u in core.take_updates() {
71 if let Some(tx) = self
72 .waiters
73 .lock()
74 .expect("waiters poisoned")
75 .remove(&u.sub)
76 {
77 let _ = tx.send(u.result);
78 }
79 }
80 r
81 };
82 let mut events = sink.drain();
83 // Lots to units. The lot of an instrument is read before its subscription is made, so it is
84 // known here. An event of an instrument without one would be in lots: it is dropped, not guessed.
85 events.retain_mut(|e| match event_instrument(&e.payload) {
86 None => true,
87 Some(id) => match self.lots.get(id) {
88 Some(lot) => {
89 scale_event(&mut e.payload, lot);
90 true
91 }
92 None => {
93 warn!(instrument = %id.key.display(), "no lot size, event dropped");
94 false
95 }
96 },
97 });
98 self.emitter.emit(events);
99 r
100 }
101
102 /// The lot size of an instrument: from the book, else one `GetInstrumentBy`, remembered.
103 async fn lot_of(&self, id: &InstrumentId) -> Result<i64, Error> {
104 require_native(id)?;
105 if let Some(lot) = self.lots.get(id) {
106 return Ok(lot);
107 }
108 self.limiter
109 .acquire("GetInstrumentBy", brokapi_runtime::now())?;
110 let mut c = self.client.instruments();
111 let r = query(
112 self.request_timeout,
113 c.get_instrument_by(instrument_request(id)),
114 )
115 .await?;
116 let i = r.instrument.ok_or_else(|| {
117 Error::not_found(format!("instrument {} not found", id.key.display()))
118 })?;
119 self.lots.put_instrument(id, &i);
120 Ok(i64::from(i.lot).max(1))
121 }
122
123 fn forward(core: &mut MdCore, tx: &mpsc::UnboundedSender<pb::MarketDataRequest>) {
124 while let Some(req) = core.poll_request() {
125 let _ = tx.send(req);
126 }
127 }
128
129 /// Owns the stream: open, send subscriptions, read, reconnect.
130 async fn run(self: Arc<Self>) {
131 let mut attempt = 0u32;
132 loop {
133 if self.cancel.is_cancelled() {
134 return;
135 }
136 let (req_tx, req_rx) = mpsc::unbounded_channel::<pb::MarketDataRequest>();
137 self.with_core(|c, s| {
138 c.on_frame(MdFrame::Connected, brokapi_runtime::now(), s);
139 Self::forward(c, &req_tx);
140 });
141 let mut client = self.client.market_data_stream();
142 let call =
143 client.market_data_stream(Request::new(UnboundedReceiverStream::new(req_rx)));
144 let opened = tokio::select! {
145 _ = self.cancel.cancelled() => return,
146 r = call => r,
147 };
148 let reason = match opened {
149 Err(status) => format!("open failed: {}: {}", status.code(), status.message()),
150 Ok(resp) => {
151 attempt = 0;
152 self.last_error.clear();
153 match self.read_loop(resp.into_inner(), &req_tx).await {
154 Some(reason) => reason,
155 None => return,
156 }
157 }
158 };
159 warn!(%reason, "market data stream down, reconnecting");
160 self.last_error.set(format!("market data stream: {reason}"));
161 drop(req_tx);
162 if !sleep_or_cancel(backoff(attempt), &self.cancel).await {
163 return;
164 }
165 attempt = attempt.saturating_add(1);
166 }
167 }
168
169 /// `None` = cancelled, `Some(reason)` = the stream ended.
170 async fn read_loop(
171 &self,
172 mut stream: tonic::Streaming<pb::MarketDataResponse>,
173 req_tx: &mpsc::UnboundedSender<pb::MarketDataRequest>,
174 ) -> Option<String> {
175 let mut last_rx = tokio::time::Instant::now();
176 loop {
177 let retry_in = {
178 let core = self.core.lock().expect("md core poisoned");
179 core.poll_timeout()
180 .map(|t| t.saturating_duration_since(brokapi_runtime::now()))
181 };
182 let backlog = self.emitter.has_backlog();
183 tokio::select! {
184 _ = self.cancel.cancelled() => return None,
185 _ = self.notify.notified() => {
186 self.with_core(|c, _| Self::forward(c, req_tx));
187 }
188 m = stream.message() => match m {
189 Ok(Some(msg)) => {
190 last_rx = tokio::time::Instant::now();
191 self.with_core(|c, s| {
192 c.on_frame(MdFrame::Response(msg), brokapi_runtime::now(), s);
193 Self::forward(c, req_tx);
194 });
195 }
196 Ok(None) => return Some("closed by the server".into()),
197 Err(status) => return Some(format!("{}: {}", status.code(), status.message())),
198 },
199 _ = tokio::time::sleep_until(last_rx + self.stale_after) => {
200 return Some(format!("silent for {:?}", self.stale_after));
201 }
202 _ = tokio::time::sleep(retry_in.unwrap_or(Duration::from_secs(3600))), if retry_in.is_some() => {
203 self.with_core(|c, s| {
204 c.on_timeout(brokapi_runtime::now(), s);
205 Self::forward(c, req_tx);
206 });
207 }
208 _ = tokio::time::sleep(Duration::from_millis(20)), if backlog => {
209 self.emitter.flush();
210 }
211 }
212 }
213 }
214}
215
216/// T-Bank market data connector: order books, trades, quotes, candles and the instrument directory.
217///
218/// One connector per token is enough, it is not tied to an account. It implements
219/// [`OrderBookSource`], [`MarketDataSource`] and [`InstrumentSource`]. Requests return their answer
220/// directly. Subscriptions deliver [`Envelope`]s into the channel given to [`start`](Self::start).
221///
222/// How T-Bank differs from the other brokers:
223///
224/// - All market data streams run over ONE bidirectional gRPC stream, which the connector keeps open
225/// and reopens with a growing delay after a break. After a reopen the subscriptions are sent again
226/// and an order book subscription gets an [`Event::Resync`](brokapi_core::Event::Resync).
227/// - Every order book message is a full snapshot, there are no increments.
228/// - Quotes are the best level of the order book. T-Bank has no separate quote stream.
229/// - The broker counts the levels of the order book, trades and candle volumes in LOTS. The connector multiplies by the lot
230/// size, so the events count in units of the instrument, as at every broker (the first read or subscription on an
231/// instrument looks the lot size up, one `GetInstrumentBy`). Prices are per one instrument.
232/// - Depths the broker accepts are 1, 10, 20, 30, 40 and 50. Other depths are rounded up on the wire
233/// and the events are cut to the depth you asked for.
234///
235/// # Examples
236///
237/// ```
238/// use std::time::Duration;
239/// use brokapi_core::{
240/// Event, InstrumentId, InstrumentKey, NativeId, OrderBook, OrderBookSource, ShutdownMode, SubscribeOpts,
241/// };
242/// use brokapi_runtime::BrokerLimiter;
243/// use brokapi_tbank::limits::default_limits;
244/// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
245/// use tokio::sync::mpsc;
246/// # use brokapi_mocks::tbank::{MockServer, TOKEN};
247/// # use brokapi_tbank_protocol::TbankConfig;
248///
249/// # #[tokio::main(flavor = "current_thread")]
250/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
251/// # let server = MockServer::start(Default::default()).await;
252/// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
253/// let (events_tx, mut events) = mpsc::channel(256);
254/// let limiter = BrokerLimiter::new(&default_limits());
255/// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "tbank-md"), limiter, events_tx).await?;
256///
257/// // The uid comes from the instrument directory, see `InstrumentSource::find`.
258/// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
259///
260/// // A snapshot by request.
261/// let OrderBook::Snapshot { bids, asks, .. } = md.order_book(&sber, 10).await? else {
262/// panic!("T-Bank answers a snapshot")
263/// };
264/// assert!(!bids.is_empty() && !asks.is_empty());
265///
266/// // A stream. The call returns once the broker has confirmed the subscription.
267/// let sub = md.subscribe_order_book(&sber, SubscribeOpts::conflate()).await?;
268/// let env = tokio::time::timeout(Duration::from_secs(5), async {
269/// loop {
270/// let env = events.recv().await.expect("the channel stays open");
271/// if matches!(env.payload, Event::OrderBook(_)) {
272/// break env;
273/// }
274/// }
275/// })
276/// .await?;
277/// assert_eq!(env.sub, Some(sub));
278///
279/// md.unsubscribe(sub).await?;
280/// md.shutdown(ShutdownMode::Immediate).await;
281/// # Ok(())
282/// # }
283/// ```
284pub struct MarketDataConnector {
285 inner: Arc<Shared>,
286 pump: JoinHandle<()>,
287}
288
289impl MarketDataConnector {
290 /// Builds the channel and starts the stream task.
291 ///
292 /// The task opens the market data stream in the background at once and reopens it after a break
293 /// (delay from 500 ms doubling to 30 s). `start` does not wait for it and does not fail when the
294 /// broker cannot be reached: the first request or subscription reports that. A subscription
295 /// waits for the broker to confirm it, for at most `request_timeout`.
296 ///
297 /// `limiter` should be the one limiter of the whole user, shared with every other connector
298 /// (see [`crate::limits`]). Events go to `out`.
299 ///
300 /// # Errors
301 ///
302 /// [`Error::Config`] when the token or app name is not a valid header value, or the endpoint is
303 /// not a valid URL. Nothing was started in that case.
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use brokapi_core::{Error, ShutdownMode};
309 /// use brokapi_runtime::BrokerLimiter;
310 /// use brokapi_tbank::limits::default_limits;
311 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
312 /// use tokio::sync::mpsc;
313 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
314 /// # use brokapi_tbank_protocol::TbankConfig;
315 ///
316 /// # #[tokio::main(flavor = "current_thread")]
317 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
318 /// # let server = MockServer::start(Default::default()).await;
319 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
320 /// let limiter = BrokerLimiter::new(&default_limits());
321 /// let (events_tx, _events) = mpsc::channel(256);
322 ///
323 /// // A bad endpoint is a configuration error at start.
324 /// let mut bad = cfg.clone();
325 /// bad.endpoint = "not a url".into();
326 /// let refused = MarketDataConnector::start(MarketDataConfig::new(bad, "tbank-md"), limiter.clone(), events_tx.clone()).await;
327 /// assert!(matches!(refused, Err(Error::Config(_))));
328 ///
329 /// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "tbank-md"), limiter, events_tx).await?;
330 /// md.shutdown(ShutdownMode::Immediate).await;
331 /// # Ok(())
332 /// # }
333 /// ```
334 pub async fn start(
335 cfg: MarketDataConfig,
336 limiter: Arc<BrokerLimiter>,
337 out: mpsc::Sender<Envelope>,
338 ) -> Result<Self, Error> {
339 let client = TbankClient::new(&cfg.tbank).map_err(|e| config(e.to_string()))?;
340 let ping = effective_ping(cfg.ping_interval, MD_MAX_PING);
341 let inner = Arc::new(Shared {
342 client,
343 limiter,
344 emitter: Emitter::new(cfg.source, out),
345 core: Mutex::new(MdCore::new(ping)),
346 lots: LotBook::new(),
347 waiters: Mutex::new(HashMap::new()),
348 subs: Mutex::new(Vec::new()),
349 sub_ids: SubIdGen::new(0),
350 notify: Notify::new(),
351 last_error: Arc::new(LastError::default()),
352 request_timeout: cfg.tbank.request_timeout,
353 stale_after: silence_limit(ping),
354 closing: AtomicBool::new(false),
355 cancel: CancellationToken::new(),
356 });
357 let pump = tokio::spawn(Arc::clone(&inner).run());
358 Ok(Self { inner, pump })
359 }
360
361 /// Any other method of the T-Bank API with this connector's token, endpoint and rate limiter.
362 /// See [`TbankRaw`].
363 ///
364 /// The raw layer is not blocked by the connector's shutdown gate and does not know the
365 /// connector's subscriptions. Its calls spend quota from the same limiter as the connector's own.
366 ///
367 /// # Examples
368 ///
369 /// ```
370 /// use brokapi_runtime::BrokerLimiter;
371 /// use brokapi_tbank::limits::default_limits;
372 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
373 /// use brokapi_tbank_protocol::v1 as pb;
374 /// use tokio::sync::mpsc;
375 /// # use brokapi_core::ShutdownMode;
376 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
377 /// # use brokapi_tbank_protocol::TbankConfig;
378 ///
379 /// # #[tokio::main(flavor = "current_thread")]
380 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
381 /// # let server = MockServer::start(Default::default()).await;
382 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
383 /// let (events_tx, _events) = mpsc::channel(64);
384 /// let md = MarketDataConnector::start(
385 /// MarketDataConfig::new(cfg, "tbank-md"),
386 /// BrokerLimiter::new(&default_limits()),
387 /// events_tx,
388 /// )
389 /// .await?;
390 ///
391 /// // Last prices of several instruments at once: no shared trait has this call.
392 /// let prices = md
393 /// .raw()
394 /// .call("GetLastPrices", |c| async move {
395 /// c.market_data()
396 /// .get_last_prices(pb::GetLastPricesRequest {
397 /// instrument_id: vec!["SBER_TQBR".into()],
398 /// ..Default::default()
399 /// })
400 /// .await
401 /// })
402 /// .await?;
403 /// assert_eq!(prices.last_prices.len(), 1);
404 /// # md.shutdown(ShutdownMode::Immediate).await;
405 /// # Ok(())
406 /// # }
407 /// ```
408 pub fn raw(&self) -> TbankRaw {
409 TbankRaw::new(
410 self.inner.client.clone(),
411 Arc::clone(&self.inner.limiter),
412 self.inner.request_timeout,
413 None,
414 )
415 }
416
417 /// What this connector can do.
418 ///
419 /// The order book is at most 50 levels deep ([`MarketDataCaps::max_order_book_depth`]). Every stream
420 /// message is a full snapshot (`incremental_order_book` is `false`). Quotes are the best level of
421 /// the book, since T-Bank has no separate quote stream. Candles stream as they form.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 /// use brokapi_runtime::BrokerLimiter;
427 /// use brokapi_tbank::limits::default_limits;
428 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
429 /// use tokio::sync::mpsc;
430 /// # use brokapi_core::ShutdownMode;
431 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
432 /// # use brokapi_tbank_protocol::TbankConfig;
433 ///
434 /// # #[tokio::main(flavor = "current_thread")]
435 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
436 /// # let server = MockServer::start(Default::default()).await;
437 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
438 /// let (events_tx, _events) = mpsc::channel(64);
439 /// let md = MarketDataConnector::start(
440 /// MarketDataConfig::new(cfg, "tbank-md"),
441 /// BrokerLimiter::new(&default_limits()),
442 /// events_tx,
443 /// )
444 /// .await?;
445 /// let caps = md.caps();
446 /// assert_eq!(caps.max_order_book_depth, 50);
447 /// assert!(!caps.incremental_order_book);
448 /// assert!(caps.streaming.quotes && caps.streaming.candles);
449 /// # md.shutdown(ShutdownMode::Immediate).await;
450 /// # Ok(())
451 /// # }
452 /// ```
453 pub fn caps(&self) -> MarketDataCaps {
454 MarketDataCaps::new(
455 market::MAX_DEPTH,
456 false,
457 StreamingCaps::new(true, true, true, true),
458 )
459 }
460
461 /// Stops the connector and its stream task.
462 ///
463 /// With [`ShutdownMode::Graceful`] the connector first unsubscribes every wire subscription and
464 /// waits for the broker to confirm, until the deadline. Subscriptions not confirmed by then count
465 /// as `unsubscribe_failed`. It also gives the event backlog the same deadline to drain into your
466 /// channel. With [`ShutdownMode::Immediate`] it just stops, and the report counts nothing.
467 ///
468 /// Market data has no commands, so `inflight_abandoned` is always empty. The method consumes the
469 /// connector, so no further call is possible. Closing the stream is the only cleanup the broker
470 /// needs.
471 ///
472 /// # Examples
473 ///
474 /// ```
475 /// use std::time::Duration;
476 /// use brokapi_core::{InstrumentId, InstrumentKey, NativeId, OrderBookSource, ShutdownMode, SubscribeOpts};
477 /// use brokapi_runtime::BrokerLimiter;
478 /// use brokapi_tbank::limits::default_limits;
479 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
480 /// use tokio::sync::mpsc;
481 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
482 /// # use brokapi_tbank_protocol::TbankConfig;
483 ///
484 /// # #[tokio::main(flavor = "current_thread")]
485 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
486 /// # let server = MockServer::start(Default::default()).await;
487 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
488 /// let (events_tx, _events) = mpsc::channel(256);
489 /// let md = MarketDataConnector::start(
490 /// MarketDataConfig::new(cfg, "tbank-md"),
491 /// BrokerLimiter::new(&default_limits()),
492 /// events_tx,
493 /// )
494 /// .await?;
495 /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
496 /// md.subscribe_order_book(&sber, SubscribeOpts::conflate()).await?;
497 ///
498 /// let report = md.shutdown(ShutdownMode::Graceful { deadline: Duration::from_secs(2) }).await;
499 /// assert!(report.inflight_abandoned.is_empty());
500 /// println!("unsubscribed {}, failed {}", report.unsubscribed, report.unsubscribe_failed);
501 /// # Ok(())
502 /// # }
503 /// ```
504 pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport {
505 let inner = &self.inner;
506 inner.closing.store(true, Ordering::SeqCst);
507 let (mut ok, mut failed) = (0, 0);
508 if let ShutdownMode::Graceful { deadline } = mode {
509 // Protocol unsubscribe for every wire subscription, then wait for the confirmations.
510 inner.subs.lock().expect("subs poisoned").clear();
511 inner.with_core(|c, _| c.apply(&SubscriptionSet::new()));
512 inner.notify.notify_one();
513 let end = brokapi_runtime::now() + deadline;
514 loop {
515 let pending = inner
516 .core
517 .lock()
518 .expect("md core poisoned")
519 .unsubscribe_progress()
520 .2;
521 if pending == 0 || brokapi_runtime::now() >= end {
522 break;
523 }
524 tokio::time::sleep(Duration::from_millis(10)).await;
525 }
526 {
527 let mut core = inner.core.lock().expect("md core poisoned");
528 core.give_up_unsubscribing();
529 let (o, f, _) = core.unsubscribe_progress();
530 ok = o;
531 failed = f;
532 }
533 let end = brokapi_runtime::now() + deadline;
534 while inner.emitter.has_backlog() && brokapi_runtime::now() < end {
535 inner.emitter.flush();
536 tokio::time::sleep(Duration::from_millis(5)).await;
537 }
538 }
539 inner.cancel.cancel();
540 let _ = self.pump.await;
541 ShutdownReport::new(ok, failed, Vec::new())
542 }
543
544 fn acquire(&self, method: &str) -> Result<(), Error> {
545 if self.inner.closing.load(Ordering::SeqCst) {
546 return Err(Error::ShuttingDown);
547 }
548 self.inner.limiter.acquire(method, brokapi_runtime::now())
549 }
550
551 /// Registers a logical subscription and waits until the broker confirms (or refuses) it.
552 async fn subscribe(
553 &self,
554 instrument: &InstrumentId,
555 kind: SubscriptionKind,
556 opts: SubscribeOpts,
557 ) -> Result<SubscriptionId, Error> {
558 // The lot size first: the events of this subscription are scaled with it.
559 self.inner.lot_of(instrument).await?;
560 self.acquire("SubscribeMarketData")?;
561 let inner = &self.inner;
562 let id = inner.sub_ids.next();
563 inner
564 .emitter
565 .register(id, Some(instrument.clone()), opts.backpressure);
566 let (tx, rx) = oneshot::channel();
567 inner
568 .waiters
569 .lock()
570 .expect("waiters poisoned")
571 .insert(id, tx);
572 self.set_desired(|subs| subs.push(DesiredSubscription { id, kind }));
573
574 let outcome = tokio::time::timeout(inner.request_timeout, rx).await;
575 match outcome {
576 Ok(Ok(Ok(()))) => Ok(id),
577 Ok(Ok(Err(reason))) => {
578 self.drop_sub(id);
579 Err(Error::Rejected {
580 code: reason,
581 message: format!("subscription to {} refused", instrument.key.display()),
582 })
583 }
584 _ => {
585 self.drop_sub(id);
586 let why = inner
587 .last_error
588 .get()
589 .unwrap_or_else(|| "no answer from the stream".into());
590 Err(transport(format!(
591 "subscription to {} not confirmed within {:?} ({why})",
592 instrument.key.display(),
593 inner.request_timeout
594 )))
595 }
596 }
597 }
598
599 fn set_desired(&self, f: impl FnOnce(&mut Vec<DesiredSubscription>)) {
600 let set = {
601 let mut subs = self.inner.subs.lock().expect("subs poisoned");
602 f(&mut subs);
603 SubscriptionSet::with_subscriptions(subs.clone())
604 };
605 self.inner.with_core(|c, _| c.apply(&set));
606 self.inner.notify.notify_one();
607 }
608
609 fn drop_sub(&self, id: SubscriptionId) {
610 self.inner
611 .waiters
612 .lock()
613 .expect("waiters poisoned")
614 .remove(&id);
615 self.set_desired(|subs| subs.retain(|s| s.id != id));
616 self.inner.emitter.unregister(id);
617 }
618
619 fn id_ref(id: &InstrumentId) -> String {
620 instrument_ref(id)
621 }
622}
623
624#[async_trait]
625impl brokapi_core::Connector for MarketDataConnector {
626 type Caps = brokapi_core::MarketDataCaps;
627
628 fn caps(&self) -> brokapi_core::MarketDataCaps {
629 MarketDataConnector::caps(self)
630 }
631
632 async fn shutdown(self, mode: brokapi_core::ShutdownMode) -> brokapi_core::ShutdownReport {
633 MarketDataConnector::shutdown(self, mode).await
634 }
635}
636
637#[async_trait]
638impl OrderBookSource for MarketDataConnector {
639 /// One snapshot by request (`GetOrderBook`).
640 ///
641 /// `depth` is the number of levels you want. It is sent rounded up to a depth the broker accepts
642 /// (1, 10, 20, 30, 40 or 50) and the answer is cut back to `depth` levels, best first. A depth of 0
643 /// counts as 1 and a depth above 50 is capped at 50 on the wire. Quantities are in units of the instrument (the connector converts from lots).
644 ///
645 /// # Errors
646 ///
647 /// - [`Error::ShuttingDown`] after shutdown started.
648 /// - [`Error::RateLimited`] when the local limiter or the broker refuses for rate. Repeat later.
649 /// - [`Error::Rejected`] when the broker does not know the instrument (code `not_found`).
650 /// - [`Error::Transport`] on a timeout or a network failure. A read, so repeat it later.
651 /// - [`Error::Config`] when the token is not accepted.
652 ///
653 /// # Examples
654 ///
655 /// ```
656 /// use brokapi_core::{Error, InstrumentId, InstrumentKey, NativeId, OrderBook, OrderBookSource, ShutdownMode};
657 /// use brokapi_runtime::BrokerLimiter;
658 /// use brokapi_tbank::limits::default_limits;
659 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
660 /// use tokio::sync::mpsc;
661 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
662 /// # use brokapi_tbank_protocol::TbankConfig;
663 ///
664 /// # #[tokio::main(flavor = "current_thread")]
665 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
666 /// # let server = MockServer::start(Default::default()).await;
667 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
668 /// let (events_tx, _events) = mpsc::channel(256);
669 /// let md = MarketDataConnector::start(
670 /// MarketDataConfig::new(cfg, "tbank-md"),
671 /// BrokerLimiter::new(&default_limits()),
672 /// events_tx,
673 /// )
674 /// .await?;
675 /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
676 ///
677 /// let OrderBook::Snapshot { bids, .. } = md.order_book(&sber, 3).await? else { panic!("a snapshot") };
678 /// assert!(bids.len() <= 3);
679 ///
680 /// // An instrument the broker does not know is a refusal, not a transport error.
681 /// let nobody = InstrumentId::new(InstrumentKey::new("NOPE", "TQBR"), NativeId::Uid("no-such-uid".into()));
682 /// assert!(matches!(md.order_book(&nobody, 3).await, Err(Error::Rejected { .. })));
683 /// # md.shutdown(ShutdownMode::Immediate).await;
684 /// # Ok(())
685 /// # }
686 /// ```
687 async fn order_book(&self, id: &InstrumentId, depth: u32) -> Result<OrderBook, Error> {
688 let lot = self.inner.lot_of(id).await?;
689 self.acquire("GetOrderBook")?;
690 let req = pb::GetOrderBookRequest {
691 depth: market::wire_depth(depth) as i32,
692 instrument_id: Some(Self::id_ref(id)),
693 ..Default::default()
694 };
695 let mut c = self.inner.client.market_data();
696 let r = query(self.inner.request_timeout, c.get_order_book(req)).await?;
697 let mut book = market::book_snapshot(
698 id,
699 &r.bids,
700 &r.asks,
701 depth.max(1) as usize,
702 r.orderbook_ts.as_ref(),
703 brokapi_runtime::now(),
704 );
705 scale_book(&mut book, brokapi_core::Decimal::from(lot));
706 Ok(book)
707 }
708
709 /// Streams the order book as full snapshots.
710 ///
711 /// `opts.depth` of `None` means 20 levels here (not the 50 maximum). Other values are clamped to
712 /// 1..=50 and sent rounded up to a depth the broker accepts. Events are cut to the depth you asked
713 /// for. Two subscriptions to one book share one wire subscription (the deeper one wins). The
714 /// call returns after the broker confirmed the subscription. After a reconnect you get an
715 /// [`Event::Resync`](brokapi_core::Event::Resync) and then fresh snapshots.
716 ///
717 /// # Errors
718 ///
719 /// - [`Error::ShuttingDown`] after shutdown started.
720 /// - [`Error::RateLimited`] when the subscription quota (`SubscribeMarketData`) is used up.
721 /// - [`Error::Rejected`] when the broker refuses the subscription (the code is the broker's
722 /// subscription status, for example `SUBSCRIPTION_STATUS_INSTRUMENT_NOT_FOUND`).
723 /// - [`Error::Transport`] when the broker did not confirm within `request_timeout` (the message
724 /// says why the stream is down, if it is).
725 ///
726 /// # Examples
727 ///
728 /// ```
729 /// use brokapi_core::{Error, InstrumentId, InstrumentKey, NativeId, OrderBookSource, ShutdownMode, SubscribeOpts};
730 /// use brokapi_runtime::BrokerLimiter;
731 /// use brokapi_tbank::limits::default_limits;
732 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
733 /// use tokio::sync::mpsc;
734 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
735 /// # use brokapi_tbank_protocol::TbankConfig;
736 ///
737 /// # #[tokio::main(flavor = "current_thread")]
738 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
739 /// # let server = MockServer::start(Default::default()).await;
740 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
741 /// let (events_tx, _events) = mpsc::channel(256);
742 /// let md = MarketDataConnector::start(
743 /// MarketDataConfig::new(cfg, "tbank-md"),
744 /// BrokerLimiter::new(&default_limits()),
745 /// events_tx,
746 /// )
747 /// .await?;
748 /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
749 ///
750 /// let opts = SubscribeOpts { depth: Some(10), ..SubscribeOpts::conflate() };
751 /// let sub = md.subscribe_order_book(&sber, opts).await?;
752 /// md.unsubscribe(sub).await?;
753 /// // A second unsubscribe is a refusal: the subscription is gone.
754 /// assert!(matches!(md.unsubscribe(sub).await, Err(Error::Rejected { .. })));
755 /// # md.shutdown(ShutdownMode::Immediate).await;
756 /// # Ok(())
757 /// # }
758 /// ```
759 async fn subscribe_order_book(
760 &self,
761 id: &InstrumentId,
762 opts: SubscribeOpts,
763 ) -> Result<SubscriptionId, Error> {
764 let depth = opts
765 .depth
766 .unwrap_or(DEFAULT_DEPTH)
767 .clamp(1, market::MAX_DEPTH);
768 self.subscribe(
769 id,
770 SubscriptionKind::OrderBook {
771 instrument: id.clone(),
772 depth,
773 },
774 opts,
775 )
776 .await
777 }
778
779 /// Ends a subscription made by any `subscribe_*` method.
780 ///
781 /// The logical subscription is dropped at once and its buffer is closed. The wire subscription is
782 /// dropped when no other subscription uses it. Events already queued for you may still arrive.
783 ///
784 /// # Errors
785 ///
786 /// [`Error::Rejected`] with code `unknown_subscription` when the id is not active (never made,
787 /// or already ended).
788 async fn unsubscribe(&self, sub: SubscriptionId) -> Result<(), Error> {
789 let known = self
790 .inner
791 .subs
792 .lock()
793 .expect("subs poisoned")
794 .iter()
795 .any(|s| s.id == sub);
796 if !known {
797 return Err(Error::Rejected {
798 code: "unknown_subscription".into(),
799 message: format!("subscription {} is not active", sub.value()),
800 });
801 }
802 self.drop_sub(sub);
803 Ok(())
804 }
805}
806
807#[async_trait]
808impl MarketDataSource for MarketDataConnector {
809 /// Anonymous market trades of the last hour, the newest `limit` of them (`GetLastTrades`).
810 ///
811 /// One hour is all that T-Bank guarantees, so older trades are not available here. Quantities
812 /// are in units of the instrument (the connector converts from lots). Trades without a direction are skipped, so you may get fewer than `limit`.
813 ///
814 /// # Errors
815 ///
816 /// The same as [`order_book`](OrderBookSource::order_book).
817 async fn last_trades(&self, id: &InstrumentId, limit: u32) -> Result<Vec<MarketTrade>, Error> {
818 let lot = self.inner.lot_of(id).await?;
819 self.acquire("GetLastTrades")?;
820 let now = crate::ids::wall_nanos();
821 let req = pb::GetLastTradesRequest {
822 from: Some(nanos_to_proto(UnixNanos::from_nanos(
823 now.as_nanos() - 3_600_000_000_000,
824 ))),
825 to: Some(nanos_to_proto(now)),
826 instrument_id: Some(Self::id_ref(id)),
827 ..Default::default()
828 };
829 let mut c = self.inner.client.market_data();
830 let r = query(self.inner.request_timeout, c.get_last_trades(req)).await?;
831 let rx = brokapi_runtime::now();
832 let mut trades: Vec<MarketTrade> = r
833 .trades
834 .iter()
835 .filter_map(|t| market::trade(id, t, rx))
836 .collect();
837 trades
838 .iter_mut()
839 .for_each(|t| scale_trade(t, brokapi_core::Decimal::from(lot)));
840 let keep = limit as usize;
841 if trades.len() > keep {
842 trades.drain(..trades.len() - keep);
843 }
844 Ok(trades)
845 }
846
847 /// Streams anonymous market trades as they happen.
848 ///
849 /// Each event is one trade. Quantities are in units of the instrument (the connector converts from lots). Trades without a direction are skipped.
850 ///
851 /// # Errors
852 ///
853 /// The same as [`subscribe_order_book`](OrderBookSource::subscribe_order_book).
854 async fn subscribe_market_trades(
855 &self,
856 id: &InstrumentId,
857 opts: SubscribeOpts,
858 ) -> Result<SubscriptionId, Error> {
859 self.subscribe(
860 id,
861 SubscriptionKind::LastTrades {
862 instrument: id.clone(),
863 },
864 opts,
865 )
866 .await
867 }
868
869 /// Best bid and ask: the first level of the order book stream.
870 ///
871 /// T-Bank has no quote stream of its own, so this subscribes to a one-level order book. Every
872 /// order book message produces one quote event.
873 ///
874 /// # Errors
875 ///
876 /// The same as [`subscribe_order_book`](OrderBookSource::subscribe_order_book).
877 async fn subscribe_quotes(
878 &self,
879 id: &InstrumentId,
880 opts: SubscribeOpts,
881 ) -> Result<SubscriptionId, Error> {
882 self.subscribe(
883 id,
884 SubscriptionKind::Quotes {
885 instrument: id.clone(),
886 },
887 opts,
888 )
889 .await
890 }
891
892 /// Historic candles of `range` (`GetCandles`).
893 ///
894 /// `close_ts` of each candle is its open time plus the length of the timeframe (a month is taken
895 /// as 30 days). Volumes are in units of the instrument (the connector converts from lots). How many candles one call returns, and the maximum range per
896 /// timeframe, are broker limits that this method does not split: not verified against the live
897 /// API.
898 ///
899 /// # Errors
900 ///
901 /// The same as [`order_book`](OrderBookSource::order_book).
902 ///
903 /// # Examples
904 ///
905 /// ```
906 /// use brokapi_core::{
907 /// InstrumentId, InstrumentKey, MarketDataSource, NativeId, ShutdownMode, TimeRange, Timeframe, UnixNanos,
908 /// };
909 /// use brokapi_runtime::BrokerLimiter;
910 /// use brokapi_tbank::limits::default_limits;
911 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
912 /// use tokio::sync::mpsc;
913 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
914 /// # use brokapi_tbank_protocol::TbankConfig;
915 ///
916 /// # #[tokio::main(flavor = "current_thread")]
917 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
918 /// # let server = MockServer::start(Default::default()).await;
919 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
920 /// let (events_tx, _events) = mpsc::channel(256);
921 /// let md = MarketDataConnector::start(
922 /// MarketDataConfig::new(cfg, "tbank-md"),
923 /// BrokerLimiter::new(&default_limits()),
924 /// events_tx,
925 /// )
926 /// .await?;
927 /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
928 ///
929 /// let range = TimeRange::new(UnixNanos::from_nanos(1_700_000_000_000_000_000), UnixNanos::from_nanos(1_700_003_600_000_000_000));
930 /// let candles = md.candles(&sber, Timeframe::M5, range).await?;
931 /// assert!(candles.iter().all(|c| c.high >= c.low));
932 ///
933 /// // Newest trades of the last hour, at most `limit` of them.
934 /// let trades = md.last_trades(&sber, 2).await?;
935 /// assert!(trades.len() <= 2);
936 /// # md.shutdown(ShutdownMode::Immediate).await;
937 /// # Ok(())
938 /// # }
939 /// ```
940 async fn candles(
941 &self,
942 id: &InstrumentId,
943 tf: Timeframe,
944 range: TimeRange,
945 ) -> Result<Vec<Candle>, Error> {
946 let lot = self.inner.lot_of(id).await?;
947 self.acquire("GetCandles")?;
948 let req = pb::GetCandlesRequest {
949 from: Some(nanos_to_proto(range.from)),
950 to: Some(nanos_to_proto(range.to)),
951 interval: market::candle_interval(tf) as i32,
952 instrument_id: Some(Self::id_ref(id)),
953 ..Default::default()
954 };
955 let mut c = self.inner.client.market_data();
956 let r = query(self.inner.request_timeout, c.get_candles(req)).await?;
957 let rx = brokapi_runtime::now();
958 let mut candles: Vec<Candle> = r
959 .candles
960 .iter()
961 .map(|x| market::historic_candle(id, tf, x, rx))
962 .collect();
963 candles
964 .iter_mut()
965 .for_each(|c| scale_candle(c, brokapi_core::Decimal::from(lot)));
966 Ok(candles)
967 }
968
969 /// The longest span one `GetCandles` request may cover, per timeframe, from the T-Invest
970 /// documentation page "Исторические котировки" (1 minute: 1 day, 5 and 10 minutes: 1 week,
971 /// 15 and 30 minutes: 3 weeks, 1 and 4 hours: 3 months, 1 day: 6 years, 1 week: 5 years,
972 /// 1 month: 10 years). The broker calls the limits approximate. Not checked against the live API.
973 fn candles_max_span(&self, tf: Timeframe) -> Option<Duration> {
974 const DAY: u64 = 86_400;
975 let days = match tf {
976 Timeframe::M1 => 1,
977 Timeframe::M5 | Timeframe::M10 => 7,
978 Timeframe::M15 | Timeframe::M30 => 21,
979 Timeframe::H1 | Timeframe::H4 => 90,
980 Timeframe::D1 => 6 * 365,
981 Timeframe::W1 => 5 * 365,
982 Timeframe::MN => 10 * 365,
983 };
984 Some(Duration::from_secs(days * DAY))
985 }
986
987 /// Streams the candle that is forming, for timeframe `tf`.
988 ///
989 /// Each event is the current state of the last candle, not only closed ones. Volumes are in units of the instrument (the connector converts from lots).
990 ///
991 /// # Errors
992 ///
993 /// The same as [`subscribe_order_book`](OrderBookSource::subscribe_order_book).
994 async fn subscribe_candles(
995 &self,
996 id: &InstrumentId,
997 tf: Timeframe,
998 opts: SubscribeOpts,
999 ) -> Result<SubscriptionId, Error> {
1000 self.subscribe(
1001 id,
1002 SubscriptionKind::LastCandle {
1003 instrument: id.clone(),
1004 timeframe: tf,
1005 },
1006 opts,
1007 )
1008 .await
1009 }
1010}
1011
1012impl MarketDataConnector {
1013 /// Whole-kind listing. Base status: what the API can trade (the default of the broker).
1014 async fn list_kind(&self, kind: InstrumentKind) -> Result<Vec<Instrument>, Error> {
1015 let method = match kind {
1016 InstrumentKind::Stock => "Shares",
1017 InstrumentKind::Bond => "Bonds",
1018 InstrumentKind::Etf => "Etfs",
1019 InstrumentKind::Future => "Futures",
1020 InstrumentKind::Option => "Options",
1021 InstrumentKind::Currency => "Currencies",
1022 InstrumentKind::MutualFund => {
1023 return Err(Error::Unsupported {
1024 capability: "find_by_kind: T-Bank has no listing of mutual funds".into(),
1025 });
1026 }
1027 InstrumentKind::Perpetual => {
1028 return Err(Error::Unsupported {
1029 capability: "find_by_kind: T-Bank has no perpetual futures".into(),
1030 });
1031 }
1032 InstrumentKind::Other => {
1033 return Err(Error::Unsupported {
1034 capability: "find_by_kind: kind Other has no T-Bank listing (indicatives, assets are not requested)".into(),
1035 });
1036 }
1037 };
1038 self.acquire(method)?;
1039 let req = pb::InstrumentsRequest {
1040 instrument_status: Some(pb::InstrumentStatus::Base as i32),
1041 instrument_exchange: None,
1042 };
1043 let t = self.inner.request_timeout;
1044 let mut c = self.inner.client.instruments();
1045 Ok(match kind {
1046 InstrumentKind::Stock => query(t, c.shares(req))
1047 .await?
1048 .instruments
1049 .iter()
1050 .map(map_instrument::from_share)
1051 .collect(),
1052 InstrumentKind::Bond => query(t, c.bonds(req))
1053 .await?
1054 .instruments
1055 .iter()
1056 .map(map_instrument::from_bond)
1057 .collect(),
1058 InstrumentKind::Etf => query(t, c.etfs(req))
1059 .await?
1060 .instruments
1061 .iter()
1062 .map(map_instrument::from_etf)
1063 .collect(),
1064 InstrumentKind::Future => query(t, c.futures(req))
1065 .await?
1066 .instruments
1067 .iter()
1068 .map(map_instrument::from_future)
1069 .collect(),
1070 InstrumentKind::Option => {
1071 // `Options` is marked deprecated in the proto, but its replacement `OptionsBy` demands a
1072 // base asset, which a whole-kind query does not have. If T-Bank removes it, this becomes
1073 // Unsupported.
1074 #[allow(deprecated)]
1075 let r = c.options(req);
1076 query(t, r)
1077 .await?
1078 .instruments
1079 .iter()
1080 .map(map_instrument::from_option)
1081 .collect()
1082 }
1083 _ => query(t, c.currencies(req))
1084 .await?
1085 .instruments
1086 .iter()
1087 .map(map_instrument::from_currency)
1088 .collect(),
1089 })
1090 }
1091
1092 async fn instrument_by(&self, id: &InstrumentId) -> Result<pb::Instrument, Error> {
1093 require_native(id)?;
1094 self.acquire("GetInstrumentBy")?;
1095 let mut c = self.inner.client.instruments();
1096 let r = query(
1097 self.inner.request_timeout,
1098 c.get_instrument_by(instrument_request(id)),
1099 )
1100 .await?;
1101 let i = r.instrument.ok_or_else(|| {
1102 Error::not_found(format!("instrument {} not found", id.key.display()))
1103 })?;
1104 self.inner.lots.put_instrument(id, &i);
1105 Ok(i)
1106 }
1107}
1108
1109#[async_trait]
1110impl InstrumentSource for MarketDataConnector {
1111 /// Searches the instrument directory by ticker, ISIN or kind.
1112 ///
1113 /// `ByTicker` / `ByIsin` use `FindInstrument` (short records: `currency` is empty, use `get`).
1114 /// `ByKind` calls the listing of that kind: `Shares`, `Bonds`, `Etfs`, `Futures`, `Options`,
1115 /// `Currencies` (base list, the instruments tradable through the API). They are heavy (a few thousand
1116 /// records) and limited to 15 requests a minute: the limiter has its own bucket `instruments_list`,
1117 /// nothing is cached here. `MutualFund` / `Other` are `Unsupported`: T-Bank has no listing for them.
1118 ///
1119 /// # Errors
1120 ///
1121 /// - [`Error::Unsupported`] for `MutualFund` and `Other`, without a network call.
1122 /// - [`Error::RateLimited`] when a quota is used up. The listing quota is small (15 a minute).
1123 /// - [`Error::Transport`], [`Error::Config`] and [`Error::Rejected`] as for
1124 /// [`order_book`](OrderBookSource::order_book).
1125 ///
1126 /// # Examples
1127 ///
1128 /// ```
1129 /// use brokapi_core::{
1130 /// Error, InstrumentId, InstrumentKey, InstrumentKind, InstrumentQuery, InstrumentSource, NativeId,
1131 /// ShutdownMode,
1132 /// };
1133 /// use brokapi_runtime::BrokerLimiter;
1134 /// use brokapi_tbank::limits::default_limits;
1135 /// use brokapi_tbank::{MarketDataConfig, MarketDataConnector};
1136 /// use tokio::sync::mpsc;
1137 /// # use brokapi_mocks::tbank::{MockServer, TOKEN};
1138 /// # use brokapi_tbank_protocol::TbankConfig;
1139 ///
1140 /// # #[tokio::main(flavor = "current_thread")]
1141 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1142 /// # let server = MockServer::start(Default::default()).await;
1143 /// # let cfg = TbankConfig::for_mock(TOKEN, &server.endpoint);
1144 /// let (events_tx, _events) = mpsc::channel(256);
1145 /// let md = MarketDataConnector::start(
1146 /// MarketDataConfig::new(cfg, "tbank-md"),
1147 /// BrokerLimiter::new(&default_limits()),
1148 /// events_tx,
1149 /// )
1150 /// .await?;
1151 /// let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
1152 ///
1153 /// // Search by ticker. A short record has no currency: use `get` for the full one.
1154 /// let found = md
1155 /// .find(InstrumentQuery::ByTicker { ticker: "SBER".into(), exchange: None })
1156 /// .await?;
1157 /// assert_eq!(found[0].id.key.symbol, "SBER");
1158 /// let full = md.get(&found[0].id).await?;
1159 /// assert_eq!(full.currency, "rub");
1160 ///
1161 /// let params = md.trading_params(&sber).await?;
1162 /// assert_eq!(params.min_qty.to_string(), "10");
1163 ///
1164 /// // T-Bank has no listing of mutual funds.
1165 /// let funds = md.find(InstrumentQuery::ByKind(InstrumentKind::MutualFund)).await;
1166 /// assert!(matches!(funds, Err(Error::Unsupported { .. })));
1167 /// # md.shutdown(ShutdownMode::Immediate).await;
1168 /// # Ok(())
1169 /// # }
1170 /// ```
1171 async fn find(&self, q: InstrumentQuery) -> Result<Vec<Instrument>, Error> {
1172 let (text, ticker, class, isin) = match q {
1173 InstrumentQuery::ByTicker { ticker, exchange } => {
1174 (ticker.clone(), Some(ticker), exchange, None)
1175 }
1176 InstrumentQuery::ByIsin(isin) => (isin.clone(), None, None, Some(isin)),
1177 InstrumentQuery::ByKind(kind) => return self.list_kind(kind).await,
1178 };
1179 self.acquire("FindInstrument")?;
1180 let req = pb::FindInstrumentRequest {
1181 query: text,
1182 instrument_kind: None,
1183 api_trade_available_flag: None,
1184 };
1185 let mut c = self.inner.client.instruments();
1186 let r = query(self.inner.request_timeout, c.find_instrument(req)).await?;
1187 Ok(r.instruments
1188 .iter()
1189 .filter(|i| {
1190 ticker
1191 .as_ref()
1192 .is_none_or(|t| i.ticker.eq_ignore_ascii_case(t))
1193 })
1194 .filter(|i| {
1195 class
1196 .as_ref()
1197 .is_none_or(|c| i.class_code.eq_ignore_ascii_case(c))
1198 })
1199 .filter(|i| isin.as_ref().is_none_or(|x| i.isin.eq_ignore_ascii_case(x)))
1200 .map(map_instrument::instrument_from_short)
1201 .collect())
1202 }
1203
1204 /// The full record of one instrument (`GetInstrumentBy`), including its currency.
1205 ///
1206 /// # Errors
1207 ///
1208 /// [`Error::Rejected`] (code `not_found`) when the broker does not know the instrument, and the
1209 /// other errors of [`order_book`](OrderBookSource::order_book).
1210 async fn get(&self, id: &InstrumentId) -> Result<Instrument, Error> {
1211 Ok(map_instrument::instrument_from_full(
1212 &self.instrument_by(id).await?,
1213 ))
1214 }
1215
1216 /// Lot size, price step and currency of an instrument.
1217 ///
1218 /// `lot_size` is the units of the instrument in one lot. `min_qty` is one lot too: an order is a whole
1219 /// number of lots. `price_step` and `price_step_value` are both the instrument's minimal price increment
1220 /// (for futures the step in money would need another call, which is not made).
1221 ///
1222 /// # Errors
1223 ///
1224 /// The same as [`get`](InstrumentSource::get).
1225 async fn trading_params(&self, id: &InstrumentId) -> Result<TradingParams, Error> {
1226 let i = self.instrument_by(id).await?;
1227 Ok(map_instrument::trading_params(id, &i))
1228 }
1229
1230 /// Whether trading is going on now (`GetTradingStatus`).
1231 ///
1232 /// The broker's status is mapped to open, break, closed or unknown. Opening and closing periods
1233 /// and auctions are `Unknown`, because orders are possible there only with restrictions. The
1234 /// raw broker status name is in `reason`.
1235 ///
1236 /// # Errors
1237 ///
1238 /// The same as [`get`](InstrumentSource::get).
1239 async fn trading_status(&self, id: &InstrumentId) -> Result<TradingStatus, Error> {
1240 require_native(id)?;
1241 self.acquire("GetTradingStatus")?;
1242 let req = pb::GetTradingStatusRequest {
1243 instrument_id: Some(Self::id_ref(id)),
1244 ..Default::default()
1245 };
1246 let mut c = self.inner.client.market_data();
1247 let r = query(self.inner.request_timeout, c.get_trading_status(req)).await?;
1248 Ok(map_instrument::trading_status(id, r.trading_status()))
1249 }
1250
1251 /// Trading sessions of an exchange in `range` (`TradingSchedules`).
1252 ///
1253 /// `exchange` is the broker's exchange name (for example `MOEX`). If the broker has no schedule
1254 /// with that name, the first one in the answer is used. An empty answer gives a schedule with no
1255 /// sessions.
1256 ///
1257 /// # Errors
1258 ///
1259 /// The same as [`order_book`](OrderBookSource::order_book).
1260 async fn schedule(&self, exchange: &str, range: TimeRange) -> Result<Schedule, Error> {
1261 self.acquire("TradingSchedules")?;
1262 let req = pb::TradingSchedulesRequest {
1263 exchange: (!exchange.is_empty()).then(|| exchange.to_string()),
1264 from: Some(nanos_to_proto(range.from)),
1265 to: Some(nanos_to_proto(range.to)),
1266 };
1267 let mut c = self.inner.client.instruments();
1268 let r = query(self.inner.request_timeout, c.trading_schedules(req)).await?;
1269 let found = r
1270 .exchanges
1271 .iter()
1272 .find(|s| s.exchange.eq_ignore_ascii_case(exchange))
1273 .or_else(|| r.exchanges.first());
1274 match found {
1275 Some(s) => Ok(map_schedule::schedule(exchange, s, brokapi_runtime::now())),
1276 None => Ok(Schedule {
1277 exchange: exchange.to_string(),
1278 sessions: Vec::new(),
1279 ts: stamps(brokapi_runtime::now(), None),
1280 }),
1281 }
1282 }
1283}