Skip to main content

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}