Skip to main content

brokapi_core/
traits.rs

1//! The capability traits: what a connector can do, split by what it needs.
2//!
3//! A market data connector implements [`OrderBookSource`], [`MarketDataSource`] and
4//! [`InstrumentSource`]. A user data connector (orders, fills, portfolio) implements
5//! [`OrderExecutor`], [`ExecutionSource`], [`PortfolioSource`] and [`StopOrderExecutor`]. Write your
6//! code against the traits and it runs on every broker.
7//!
8//! Rules that hold for all of them:
9//! - A call that reads or commands returns its answer directly. A subscription call returns a
10//!   [`SubscriptionId`] and the events come later as [`Envelope`](crate::Envelope)s in the channel
11//!   given to the connector at start.
12//! - A capability the broker lacks answers [`Error::Unsupported`] at once, without a network call.
13//!   Read the connector's caps ([`MarketDataCaps`](crate::MarketDataCaps),
14//!   [`UserDataCaps`](crate::UserDataCaps)) to know in advance.
15//! - A trading command is never repeated by the library. If the outcome is unknown the error is
16//!   [`Error::Unknown`], see there what to do.
17//! - Trading commands are refused with [`Error::NotReconciled`] until the first successful
18//!   [`OrderExecutor::reconcile`], with [`Error::Degraded`] after a lost event, and with
19//!   [`Error::ShuttingDown`] during shutdown. Reading calls are not gated by this.
20
21use async_trait::async_trait;
22
23use crate::error::Error;
24use crate::types::{
25    Balance, Candle, ClientOrderId, Execution, Instrument, InstrumentId, InstrumentQuery,
26    MarketTrade, OrderAck, OrderBook, OrderSnapshot, PlaceOrderRequest, PlaceStopOrderRequest,
27    Portfolio, Position, ReconcileReport, ReplaceOrderRequest, Schedule, ShutdownMode,
28    ShutdownReport, StopOrderRef, StopOrderSnapshot, SubscribeOpts, SubscriptionId, TimeRange,
29    Timeframe, TradingParams, TradingStatus,
30};
31
32/// What every connector offers besides its capabilities: it says what it can do, and it stops.
33///
34/// Code that is generic over a connector can ask for the capabilities without knowing the broker, and
35/// shut any connector down the same way. A market data connector has [`MarketDataCaps`](crate::MarketDataCaps),
36/// a user data connector has [`UserDataCaps`](crate::UserDataCaps). The two methods are also inherent
37/// methods of each connector, so code that names the type needs no trait in scope.
38///
39/// # Examples
40///
41/// ```
42/// use brokapi_bcs::limits::default_limits;
43/// use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
44/// use brokapi_core::{Connector, MarketDataCaps, ShutdownMode};
45/// use brokapi_runtime::BrokerLimiter;
46/// # use brokapi_bcs_protocol::config::BcsConfig;
47///
48/// /// Works with the connector of any broker.
49/// async fn depth_of<C: Connector<Caps = MarketDataCaps>>(c: C) -> u32 {
50///     let depth = c.caps().max_order_book_depth;
51///     c.shutdown(ShutdownMode::Immediate).await;
52///     depth
53/// }
54///
55/// # #[tokio::main(flavor = "current_thread")]
56/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
57/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
58/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base);
59/// let (events_tx, _events) = tokio::sync::mpsc::channel(16);
60/// let limiter = BrokerLimiter::new(&default_limits());
61/// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
62/// assert!(depth_of(md).await > 0);
63/// # Ok(())
64/// # }
65/// ```
66#[async_trait]
67pub trait Connector: Send + Sync {
68    /// The type that describes what this connector can do.
69    type Caps: Copy + Send + Sync + 'static;
70
71    /// What this connector can do. Read it before you use a feature that not every broker has.
72    fn caps(&self) -> Self::Caps;
73
74    /// Stops the connector: closes its streams, gives up commands that have no answer yet (they become
75    /// [`Error::Unknown`], see [`ShutdownReport`]) and ends its tasks. Takes the connector by value, so it
76    /// cannot be used afterwards.
77    async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport
78    where
79        Self: Sized;
80}
81
82/// Order book snapshot and stream.
83///
84/// [`order_book`](Self::order_book) reads the book once and answers an
85/// [`OrderBook::Snapshot`]. [`subscribe_order_book`](Self::subscribe_order_book) starts a stream:
86/// the books arrive as [`Event::OrderBook`](crate::Event::OrderBook) in the channel given to the
87/// connector at start, tagged with the returned [`SubscriptionId`]. After a reconnect the stream
88/// sends [`Event::Resync`](crate::Event::Resync) and a new snapshot. [`unsubscribe`](Self::unsubscribe)
89/// stops a subscription of this connector by its id.
90///
91/// # Examples
92///
93/// ```
94/// use std::time::Duration;
95/// use brokapi_bcs::limits::default_limits;
96/// use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
97/// use brokapi_core::{
98///     Event, InstrumentId, InstrumentKey, NativeId, OrderBook, OrderBookSource, Price, ShutdownMode,
99///     SubscribeOpts,
100/// };
101/// use brokapi_runtime::BrokerLimiter;
102/// # use brokapi_bcs_protocol::config::BcsConfig;
103///
104/// /// Works with the connector of any broker.
105/// async fn best_bid(src: &impl OrderBookSource, id: &InstrumentId) -> Option<Price> {
106///     match src.order_book(id, 5).await.ok()? {
107///         OrderBook::Snapshot { bids, .. } => bids.first().map(|l| l.price),
108///         _ => None,
109///     }
110/// }
111///
112/// # #[tokio::main(flavor = "current_thread")]
113/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
114/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
115/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base);
116/// let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
117/// let limiter = BrokerLimiter::new(&default_limits());
118/// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
119///
120/// let sber = InstrumentId::new(
121///     InstrumentKey::new("SBER", "TQBR"),
122///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
123/// );
124/// assert!(best_bid(&md, &sber).await.is_some());
125///
126/// // A stream: the book arrives as an envelope with the id of the subscription.
127/// let sub = md.subscribe_order_book(&sber, SubscribeOpts::conflate()).await?;
128/// let env = tokio::time::timeout(Duration::from_secs(8), events.recv()).await?.expect("channel is open");
129/// assert_eq!(env.sub, Some(sub));
130/// assert!(matches!(env.payload, Event::OrderBook(_)));
131///
132/// md.unsubscribe(sub).await?;
133/// md.shutdown(ShutdownMode::Immediate).await;
134/// # Ok(())
135/// # }
136/// ```
137#[async_trait]
138pub trait OrderBookSource: Send + Sync {
139    /// Reads the order book once, to `depth` levels per side.
140    ///
141    /// The answer is an [`OrderBook::Snapshot`]. Bids are listed best (highest) first and asks best
142    /// (lowest) first. A `depth` above [`MarketDataCaps::max_order_book_depth`](crate::MarketDataCaps::max_order_book_depth)
143    /// is cut to it (the BCS connector does so without telling).
144    ///
145    /// # Errors
146    ///
147    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
148    /// - [`Error::RateLimited`] when the local limiter or the broker refuses for now. Wait and repeat.
149    /// - [`Error::Transport`] when the network failed or the answer was unreadable. Repeat later.
150    /// - [`Error::Rejected`] when the broker refuses the request, for example an unknown instrument
151    ///   (BCS: code `validation`).
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use brokapi_core::{ErrorClass, OrderBook, OrderBookSource};
157    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
158    /// # use brokapi_core::*;
159    /// # #[tokio::main(flavor = "current_thread")]
160    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
161    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
162    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
163    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
164    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
165    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
166    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
167    /// let OrderBook::Snapshot { bids, asks, .. } = md.order_book(&sber, 3).await? else {
168    ///     panic!("a read always answers a snapshot");
169    /// };
170    /// assert!(bids.len() <= 3 && asks.len() <= 3);
171    /// // The best prices are first on both sides, so the spread is the gap between them.
172    /// assert!(bids[0].price < asks[0].price);
173    ///
174    /// // An instrument id of another broker's kind is a setup error: the call is not made.
175    /// let wrong = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Figi("BBG004730N88".into()));
176    /// let err = md.order_book(&wrong, 3).await.unwrap_err();
177    /// assert!(matches!(err, Error::Config(_)));
178    /// assert_eq!(err.class(), ErrorClass::Fatal);
179    /// # md.shutdown(ShutdownMode::Immediate).await;
180    /// # Ok(())
181    /// # }
182    /// ```
183    async fn order_book(&self, id: &InstrumentId, depth: u32) -> Result<OrderBook, Error>;
184
185    /// Starts a stream of the order book of one instrument.
186    ///
187    /// Returns when the stream is open at the broker (see **Contract**). Books then arrive as
188    /// [`Event::OrderBook`](crate::Event::OrderBook) with `Envelope::sub` equal to the returned id.
189    /// The stream restores itself after a reconnect and announces it with
190    /// [`Event::Resync`](crate::Event::Resync). `opts.depth` is the number of levels
191    /// (`None`: the connector's default, BCS uses its maximum). For books use
192    /// [`Backpressure::Conflate`](crate::Backpressure::Conflate): a slow reader then sees the latest
193    /// book and not a queue of stale ones.
194    ///
195    /// # Contract
196    ///
197    /// - **When:** at any time, no reconcile is needed (the market data connector has no trading gate).
198    /// - **Guarantee:** the call returns only when the stream is open: the broker has confirmed the
199    ///   subscription, or, where the protocol has no confirmation, the connection of the stream is up. What
200    ///   the broker sends after the return reaches you. A stream that is not open within the connector's
201    ///   `subscribe_ack_timeout` (3 seconds by default) is [`Error::Transport`], and nothing is left half
202    ///   open, so the call can be repeated.
203    ///   (Finam returns `Ok` after the wait and goes on opening the stream, see
204    ///   [`OrderExecutor::subscribe_order_state`].)
205    /// - **Guarantee:** the stream restores itself after a reconnect. Each restore is announced with
206    ///   [`Event::Resync`](crate::Event::Resync): drop the book you hold for that instrument and wait
207    ///   for the next snapshot.
208    ///
209    /// # Errors
210    ///
211    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
212    /// - [`Error::Unsupported`] when the connector has no order book stream
213    ///   ([`StreamingCaps::order_book`](crate::StreamingCaps::order_book) is `false`).
214    /// - [`Error::RateLimited`], [`Error::Transport`] when the subscription could not be made, also when
215    ///   the stream did not open in time. The subscription does not exist, ask again later.
216    /// - [`Error::Rejected`] with code `subscription_refused` when the broker refused it (an unknown
217    ///   instrument, no permission). Repeating the call unchanged will not help.
218    ///
219    /// # Examples
220    ///
221    /// ```
222    /// use std::time::Duration;
223    /// use brokapi_core::{Backpressure, Event, OrderBookSource, SubscribeOpts};
224    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
225    /// # use brokapi_core::*;
226    /// # #[tokio::main(flavor = "current_thread")]
227    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
228    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
229    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
230    /// # let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
231    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
232    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
233    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
234    /// // The 10 best levels, latest book wins if we read slowly.
235    /// let opts = SubscribeOpts { depth: Some(10), ..SubscribeOpts::conflate() };
236    /// assert_eq!(opts.backpressure, Backpressure::Conflate);
237    /// let sub = md.subscribe_order_book(&sber, opts).await?;
238    ///
239    /// // All subscriptions of the connector write into the one channel, so route by `sub`.
240    /// let env = tokio::time::timeout(Duration::from_secs(8), events.recv()).await?.expect("channel is open");
241    /// assert_eq!(env.sub, Some(sub));
242    /// let Event::OrderBook(book) = env.payload else { panic!("an order book subscription sends books") };
243    /// # let _ = book;
244    /// # md.shutdown(ShutdownMode::Immediate).await;
245    /// # Ok(())
246    /// # }
247    /// ```
248    async fn subscribe_order_book(
249        &self,
250        id: &InstrumentId,
251        opts: SubscribeOpts,
252    ) -> Result<SubscriptionId, Error>;
253
254    /// Stops a subscription of this connector.
255    ///
256    /// After it returns, no new events of `sub` are produced. Envelopes already in your channel stay
257    /// there, so keep reading until you have what you need. A subscription ends on its own when the
258    /// connector shuts down.
259    ///
260    /// # Errors
261    ///
262    /// - [`Error::Rejected`] with code `unknown_subscription` when `sub` is not an active subscription
263    ///   of this connector: it was never given out, or it is already stopped. The same at all brokers.
264    /// - [`Error::Transport`] when the broker could not be told.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use brokapi_core::{OrderBookSource, SubscribeOpts};
270    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
271    /// # use brokapi_core::*;
272    /// # #[tokio::main(flavor = "current_thread")]
273    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
274    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
275    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
276    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
277    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
278    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
279    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
280    /// let sub = md.subscribe_order_book(&sber, SubscribeOpts::conflate()).await?;
281    /// md.unsubscribe(sub).await?;
282    /// // The id is gone: a second unsubscribe is an error, not a silent success.
283    /// assert!(md.unsubscribe(sub).await.is_err());
284    /// # md.shutdown(ShutdownMode::Immediate).await;
285    /// # Ok(())
286    /// # }
287    /// ```
288    async fn unsubscribe(&self, sub: SubscriptionId) -> Result<(), Error>;
289}
290
291/// Public trades, quotes and candles: history reads and streams.
292///
293/// Reads ([`last_trades`](Self::last_trades), [`candles`](Self::candles)) answer directly. Streams
294/// ([`subscribe_market_trades`](Self::subscribe_market_trades),
295/// [`subscribe_quotes`](Self::subscribe_quotes), [`subscribe_candles`](Self::subscribe_candles))
296/// return a [`SubscriptionId`] and send [`Event::MarketTrade`](crate::Event::MarketTrade),
297/// [`Event::Quote`](crate::Event::Quote) and [`Event::Candle`](crate::Event::Candle) into the
298/// channel given at start. Stop a stream with [`OrderBookSource::unsubscribe`], which takes any
299/// subscription of the connector. Your own fills are not here, see [`ExecutionSource`].
300///
301/// # Examples
302///
303/// ```
304/// use std::time::Duration;
305/// use brokapi_bcs::limits::default_limits;
306/// use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
307/// use brokapi_core::{
308///     Event, InstrumentId, InstrumentKey, MarketDataSource, NativeId, ShutdownMode, SubscribeOpts,
309///     TimeRange, Timeframe, UnixNanos,
310/// };
311/// use brokapi_runtime::BrokerLimiter;
312/// # use brokapi_bcs_protocol::config::BcsConfig;
313///
314/// # #[tokio::main(flavor = "current_thread")]
315/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
316/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
317/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base);
318/// let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
319/// let limiter = BrokerLimiter::new(&default_limits());
320/// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
321///
322/// let sber = InstrumentId::new(
323///     InstrumentKey::new("SBER", "TQBR"),
324///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
325/// );
326///
327/// // The last 2 deals of the tape, oldest first (BCS).
328/// let trades = md.last_trades(&sber, 2).await?;
329/// assert!(!trades.is_empty() && trades.len() <= 2);
330///
331/// // One day of 1-minute candles. The range is in nanoseconds since the epoch.
332/// let day = TimeRange::new(
333///     UnixNanos::from_nanos(1_792_368_000_000_000_000), // 2026-10-19 00:00 UTC
334///     UnixNanos::from_nanos(1_792_454_400_000_000_000), // 2026-10-20 00:00 UTC
335/// );
336/// let bars = md.candles(&sber, Timeframe::M1, day).await?;
337/// assert!(bars.iter().all(|c| c.low <= c.high));
338///
339/// // A stream of quotes: the best bid and ask, the latest one wins if we read slowly.
340/// let sub = md.subscribe_quotes(&sber, SubscribeOpts::conflate()).await?;
341/// let quote = tokio::time::timeout(Duration::from_secs(8), async {
342///     while let Some(env) = events.recv().await {
343///         if let (Some(s), Event::Quote(q)) = (env.sub, env.payload) {
344///             if s == sub {
345///                 return Some(q);
346///             }
347///         }
348///     }
349///     None
350/// })
351/// .await?
352/// .expect("a quote arrived");
353/// assert!(quote.bid.is_some() || quote.ask.is_some());
354///
355/// md.shutdown(ShutdownMode::Immediate).await;
356/// # Ok(())
357/// # }
358/// ```
359#[async_trait]
360pub trait MarketDataSource: Send + Sync {
361    /// Reads the newest `limit` deals of the public tape.
362    ///
363    /// How far back the tape goes depends on the broker (BCS: the current session, T-Bank: the last
364    /// hour). The BCS connector returns the deals oldest first and drops a record whose side is
365    /// unknown. You may get fewer than `limit`.
366    ///
367    /// # Errors
368    ///
369    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
370    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
371    /// - [`Error::Rejected`] when the broker refuses the request.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use brokapi_core::MarketDataSource;
377    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
378    /// # use brokapi_core::*;
379    /// # #[tokio::main(flavor = "current_thread")]
380    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
381    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
382    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
383    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
384    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
385    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
386    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
387    /// let trades = md.last_trades(&sber, 5).await?;
388    /// for t in &trades {
389    ///     assert!(t.price.is_positive() && t.qty.is_positive());
390    /// }
391    /// # md.shutdown(ShutdownMode::Immediate).await;
392    /// # Ok(())
393    /// # }
394    /// ```
395    async fn last_trades(&self, id: &InstrumentId, limit: u32) -> Result<Vec<MarketTrade>, Error>;
396
397    /// Starts a stream of the public deals of one instrument.
398    ///
399    /// Deals arrive as [`Event::MarketTrade`](crate::Event::MarketTrade) tagged with the returned
400    /// id. Every deal is an event of its own, so [`Backpressure::Conflate`](crate::Backpressure::Conflate)
401    /// would lose deals: use [`Backpressure::DropNewest`](crate::Backpressure::DropNewest) if losing
402    /// some is acceptable, or [`Backpressure::Lossless`](crate::Backpressure::Lossless) if not (an
403    /// overflow then sends [`Event::Overflow`](crate::Event::Overflow)).
404    ///
405    /// # Errors
406    ///
407    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
408    /// - [`Error::Unsupported`] when [`StreamingCaps::market_trades`](crate::StreamingCaps::market_trades)
409    ///   is `false`.
410    /// - [`Error::RateLimited`], [`Error::Transport`] when the subscription could not be made, also when
411    ///   the stream did not open in time (see [`OrderBookSource::subscribe_order_book`]: the call returns when
412    ///   the stream is open, the same for every `subscribe_*`).
413    /// - [`Error::Rejected`] with code `subscription_refused` when the broker refused it.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// use std::time::Duration;
419    /// use brokapi_core::{Backpressure, Event, MarketDataSource, SubscribeOpts};
420    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
421    /// # use brokapi_core::*;
422    /// # #[tokio::main(flavor = "current_thread")]
423    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
424    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
425    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
426    /// # let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
427    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
428    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
429    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
430    /// // Keep up to 256 deals while we are slow, drop the newest ones beyond that.
431    /// let opts = SubscribeOpts { backpressure: Backpressure::DropNewest { cap: 256 }, depth: None };
432    /// let sub = md.subscribe_market_trades(&sber, opts).await?;
433    /// let env = tokio::time::timeout(Duration::from_secs(8), events.recv()).await?.expect("channel is open");
434    /// assert_eq!(env.sub, Some(sub));
435    /// assert!(matches!(env.payload, Event::MarketTrade(_)));
436    /// # md.shutdown(ShutdownMode::Immediate).await;
437    /// # Ok(())
438    /// # }
439    /// ```
440    async fn subscribe_market_trades(
441        &self,
442        id: &InstrumentId,
443        opts: SubscribeOpts,
444    ) -> Result<SubscriptionId, Error>;
445
446    /// Starts a stream of the best bid and ask of one instrument.
447    ///
448    /// Quotes arrive as [`Event::Quote`](crate::Event::Quote) tagged with the returned id. Each
449    /// quote replaces the previous one, so [`Backpressure::Conflate`](crate::Backpressure::Conflate)
450    /// is the right policy.
451    ///
452    /// # Errors
453    ///
454    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
455    /// - [`Error::Unsupported`] when [`StreamingCaps::quotes`](crate::StreamingCaps::quotes) is `false`.
456    /// - [`Error::RateLimited`], [`Error::Transport`] when the subscription could not be made, also when
457    ///   the stream did not open in time (as in [`OrderBookSource::subscribe_order_book`]).
458    /// - [`Error::Rejected`] with code `subscription_refused` when the broker refused it.
459    ///
460    /// # Examples
461    ///
462    /// See the example of the trait, which receives a quote.
463    async fn subscribe_quotes(
464        &self,
465        id: &InstrumentId,
466        opts: SubscribeOpts,
467    ) -> Result<SubscriptionId, Error>;
468
469    /// Reads the bars of `tf` that fall in `range`.
470    ///
471    /// A bar has `open_ts` and `close_ts`, and a price is a [`Price`](crate::Price). Whether the ends
472    /// of `range` are inclusive and how far back the broker serves is decided by the broker.
473    ///
474    /// # Errors
475    ///
476    /// - [`Error::Unsupported`] when the broker has no such timeframe (BCS has no
477    ///   [`Timeframe::M10`], capability `candles_m10`). The call is not made.
478    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
479    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
480    /// - [`Error::Rejected`] when the broker refuses the request.
481    ///
482    /// # Examples
483    ///
484    /// ```
485    /// use brokapi_core::{Error, MarketDataSource, TimeRange, Timeframe, UnixNanos};
486    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
487    /// # use brokapi_core::*;
488    /// # #[tokio::main(flavor = "current_thread")]
489    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
490    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
491    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
492    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
493    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
494    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
495    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
496    /// let day = TimeRange::new(
497    ///     UnixNanos::from_nanos(1_792_368_000_000_000_000), // 2026-10-19 00:00 UTC
498    ///     UnixNanos::from_nanos(1_792_454_400_000_000_000), // 2026-10-20 00:00 UTC
499    /// );
500    /// let bars = md.candles(&sber, Timeframe::M5, day).await?;
501    /// assert!(bars.iter().all(|c| c.timeframe == Timeframe::M5));
502    ///
503    /// // BCS has no 10-minute bars. The answer comes at once, nothing is sent to the broker.
504    /// match md.candles(&sber, Timeframe::M10, day).await {
505    ///     Err(Error::Unsupported { capability }) => assert_eq!(capability, "candles_m10"),
506    ///     other => panic!("expected Unsupported, got {other:?}"),
507    /// }
508    /// # md.shutdown(ShutdownMode::Immediate).await;
509    /// # Ok(())
510    /// # }
511    /// ```
512    async fn candles(
513        &self,
514        id: &InstrumentId,
515        tf: Timeframe,
516        range: TimeRange,
517    ) -> Result<Vec<Candle>, Error>;
518
519    /// The longest span of time one [`candles`](Self::candles) request may cover for `tf`, or `None`
520    /// when the connector does not know a limit.
521    ///
522    /// Brokers cap one history request by the number of bars or by the length of the span, and a
523    /// request over the cap is refused ([`Error::Rejected`]). A long history is therefore read in
524    /// windows of at most this span. Do not slice by hand: `brokapi_runtime::history` does it, waits
525    /// out [`Error::RateLimited`] and joins the windows. The value comes from the broker's
526    /// documentation, not from a live check. Pure function, no network call.
527    ///
528    /// The default is `None` ("unknown"): the caller then chooses a window itself.
529    fn candles_max_span(&self, tf: Timeframe) -> Option<std::time::Duration> {
530        let _ = tf;
531        None
532    }
533
534    /// Starts a stream of the latest bar of `tf` for one instrument.
535    ///
536    /// Bars arrive as [`Event::Candle`](crate::Event::Candle) tagged with the returned id. Use
537    /// [`Backpressure::Conflate`](crate::Backpressure::Conflate): the latest bar replaces older ones.
538    ///
539    /// # Errors
540    ///
541    /// - [`Error::Unsupported`] when [`StreamingCaps::candles`](crate::StreamingCaps::candles) is
542    ///   `false`, or the broker has no such timeframe (BCS has no [`Timeframe::M10`]).
543    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
544    /// - [`Error::RateLimited`], [`Error::Transport`] when the subscription could not be made, also when
545    ///   the stream did not open in time (as in [`OrderBookSource::subscribe_order_book`]).
546    /// - [`Error::Rejected`] with code `subscription_refused` when the broker refused it.
547    ///
548    /// # Examples
549    ///
550    /// ```
551    /// use std::time::Duration;
552    /// use brokapi_core::{Event, MarketDataSource, SubscribeOpts, Timeframe};
553    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
554    /// # use brokapi_core::*;
555    /// # #[tokio::main(flavor = "current_thread")]
556    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
557    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
558    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
559    /// # let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
560    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
561    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
562    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
563    /// let sub = md.subscribe_candles(&sber, Timeframe::M1, SubscribeOpts::conflate()).await?;
564    /// let env = tokio::time::timeout(Duration::from_secs(8), events.recv()).await?.expect("channel is open");
565    /// assert_eq!(env.sub, Some(sub));
566    /// let Event::Candle(bar) = env.payload else { panic!("a candle subscription sends candles") };
567    /// assert_eq!(bar.timeframe, Timeframe::M1);
568    /// # md.shutdown(ShutdownMode::Immediate).await;
569    /// # Ok(())
570    /// # }
571    /// ```
572    async fn subscribe_candles(
573        &self,
574        id: &InstrumentId,
575        tf: Timeframe,
576        opts: SubscribeOpts,
577    ) -> Result<SubscriptionId, Error>;
578}
579
580/// The instrument directory and the rules for trading an instrument.
581///
582/// Public data, so it lives in the market data connector. The library does NOT check your orders
583/// against it: round quantity and price with [`TradingParams`] yourself before
584/// [`OrderExecutor::place`].
585///
586/// # Examples
587///
588/// ```
589/// # use brokapi_core::{Price, Qty};
590/// use brokapi_bcs::limits::default_limits;
591/// use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
592/// use brokapi_core::{
593///     Error, InstrumentId, InstrumentKind, InstrumentQuery, InstrumentSource, TradingAvailability,
594///     ShutdownMode,
595/// };
596/// use brokapi_runtime::BrokerLimiter;
597/// # use brokapi_bcs_protocol::config::BcsConfig;
598///
599/// # #[tokio::main(flavor = "current_thread")]
600/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
601/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
602/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base);
603/// let (events_tx, _events) = tokio::sync::mpsc::channel(256);
604/// let limiter = BrokerLimiter::new(&default_limits());
605/// let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
606///
607/// // Find an instrument by ticker. The result carries ids ready to pass to every other method.
608/// let found = md
609///     .find(InstrumentQuery::ByTicker { ticker: "SBER".into(), exchange: None })
610///     .await?;
611/// let sber: InstrumentId = found.first().expect("SBER is in the directory").id.clone();
612///
613/// // How to size and price an order.
614/// let params = md.trading_params(&sber).await?;
615/// assert!(params.lot_size > Qty::from(0) && params.price_step > Price::from(0));
616///
617/// // Is it tradable now?
618/// let status = md.trading_status(&sber).await?;
619/// println!("{}: {:?}", sber.key.display(), status.availability);
620///
621/// // A search by a kind the broker cannot list is refused at once.
622/// let by_kind = md.find(InstrumentQuery::ByKind(InstrumentKind::Option)).await;
623/// assert!(matches!(by_kind, Err(Error::Unsupported { .. })));
624///
625/// md.shutdown(ShutdownMode::Immediate).await;
626/// # Ok(())
627/// # }
628/// ```
629#[async_trait]
630pub trait InstrumentSource: Send + Sync {
631    /// Looks instruments up in the directory.
632    ///
633    /// The result can be empty (nothing matches). A query by kind can be a long list, the connector
634    /// may read it page by page, each page counts against the rate limit.
635    ///
636    /// # Errors
637    ///
638    /// - [`Error::Unsupported`] when the broker cannot list that kind (BCS: `Option` and `Other`).
639    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later. The BCS connector also
640    ///   answers `Transport` when a kind has more than its page limit, rather than return part of
641    ///   the list.
642    /// - [`Error::Rejected`] when the broker refuses the request.
643    ///
644    /// # Examples
645    ///
646    /// ```
647    /// use brokapi_core::{InstrumentQuery, InstrumentSource};
648    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
649    /// # use brokapi_core::*;
650    /// # #[tokio::main(flavor = "current_thread")]
651    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
652    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
653    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
654    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
655    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
656    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
657    /// let list = md
658    ///     .find(InstrumentQuery::ByTicker { ticker: "SBER".into(), exchange: None })
659    ///     .await?;
660    /// assert!(list.iter().any(|i| i.id.key.symbol == "SBER"));
661    /// # md.shutdown(ShutdownMode::Immediate).await;
662    /// # Ok(())
663    /// # }
664    /// ```
665    async fn find(&self, q: InstrumentQuery) -> Result<Vec<Instrument>, Error>;
666
667    /// Reads one directory entry.
668    ///
669    /// # Errors
670    ///
671    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
672    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later. An instrument the
673    ///   directory does not have is reported as `Transport` by the BCS connector (message
674    ///   `instrument not found`), there is no separate variant for "not found".
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// use brokapi_core::InstrumentSource;
680    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
681    /// # use brokapi_core::*;
682    /// # #[tokio::main(flavor = "current_thread")]
683    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
684    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
685    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
686    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
687    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
688    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
689    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
690    /// let info = md.get(&sber).await?;
691    /// assert_eq!(info.id.key.symbol, "SBER");
692    /// # md.shutdown(ShutdownMode::Immediate).await;
693    /// # Ok(())
694    /// # }
695    /// ```
696    async fn get(&self, id: &InstrumentId) -> Result<Instrument, Error>;
697
698    /// Reads lot size, price step and the other numbers needed to size and price an order.
699    ///
700    /// # Errors
701    ///
702    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
703    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later. The BCS connector also
704    ///   answers `Transport` when the directory has no trading parameters for the board.
705    ///
706    /// # Examples
707    ///
708    /// ```
709    /// # use brokapi_core::{Decimal, dec};
710    /// use brokapi_core::InstrumentSource;
711    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
712    /// # use brokapi_core::*;
713    /// # #[tokio::main(flavor = "current_thread")]
714    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
715    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
716    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
717    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
718    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
719    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
720    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
721    /// let params = md.trading_params(&sber).await?;
722    ///
723    /// // The nearest price on the grid of the instrument at or below 250.037. The rounding is exact.
724    /// let wanted = Price::new(dec!(250.037));
725    /// let price = params.round_price(wanted, RoundMode::Down)?;
726    /// assert!(price <= wanted);
727    /// assert!(params.is_price_on_grid(price));
728    /// # md.shutdown(ShutdownMode::Immediate).await;
729    /// # Ok(())
730    /// # }
731    /// ```
732    async fn trading_params(&self, id: &InstrumentId) -> Result<TradingParams, Error>;
733
734    /// Reads whether the instrument can be traded now.
735    ///
736    /// # Errors
737    ///
738    /// - [`Error::Config`] when `id.native` is of a kind this broker does not use.
739    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// use brokapi_core::{InstrumentSource, TradingAvailability};
745    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
746    /// # use brokapi_core::*;
747    /// # #[tokio::main(flavor = "current_thread")]
748    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
749    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
750    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
751    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
752    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
753    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
754    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
755    /// let status = md.trading_status(&sber).await?;
756    /// if status.availability == TradingAvailability::Open {
757    ///     // safe to send orders
758    /// }
759    /// # md.shutdown(ShutdownMode::Immediate).await;
760    /// # Ok(())
761    /// # }
762    /// ```
763    async fn trading_status(&self, id: &InstrumentId) -> Result<TradingStatus, Error>;
764
765    /// Reads the trading sessions of an exchange over `range`.
766    ///
767    /// What `exchange` means depends on the broker: T-Bank takes an exchange name (`MOEX`), Finam and
768    /// BCS take `TICKER@MARKET` because their schedules are per instrument. The BCS answer is for
769    /// one day, the day of `range.from`.
770    ///
771    /// # Errors
772    ///
773    /// - [`Error::Config`] when `exchange` is not in the form the broker needs (BCS: not
774    ///   `TICKER@CLASSCODE`).
775    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
776    ///
777    /// # Examples
778    ///
779    /// ```
780    /// use brokapi_core::{Error, InstrumentSource, TimeRange, UnixNanos};
781    /// # use brokapi_bcs::{MarketDataConfig, MarketDataConnector};
782    /// # use brokapi_core::*;
783    /// # #[tokio::main(flavor = "current_thread")]
784    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
785    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
786    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
787    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
788    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
789    /// # let md = MarketDataConnector::start(MarketDataConfig::new(cfg, "bcs-md"), limiter, events_tx).await?;
790    /// let day = TimeRange::new(
791    ///     UnixNanos::from_nanos(1_792_368_000_000_000_000), // 2026-10-19 00:00 UTC
792    ///     UnixNanos::from_nanos(1_792_454_400_000_000_000), // 2026-10-20 00:00 UTC
793    /// );
794    /// // BCS schedules are per instrument: "TICKER@CLASSCODE".
795    /// let schedule = md.schedule("SBER@TQBR", day).await?;
796    /// assert!(schedule.sessions.iter().all(|s| s.start < s.end));
797    ///
798    /// // A bare exchange name is not enough for BCS.
799    /// assert!(matches!(md.schedule("MOEX", day).await, Err(Error::Config(_))));
800    /// # md.shutdown(ShutdownMode::Immediate).await;
801    /// # Ok(())
802    /// # }
803    /// ```
804    async fn schedule(&self, exchange: &str, range: TimeRange) -> Result<Schedule, Error>;
805}
806
807/// Orders: put, cancel, replace, look at them, follow them, and reconcile with your ledger.
808///
809/// The rules that matter:
810/// - YOU make the [`ClientOrderId`] of every order, a fresh UUID, and keep it in your own ledger.
811/// - Trading commands ([`place`](Self::place), [`cancel`](Self::cancel), [`replace`](Self::replace))
812///   are refused until the first successful [`reconcile`](Self::reconcile), so start by calling
813///   `reconcile` (with an empty slice if you have no ledger).
814/// - A command that got no answer is [`Error::Unknown`] and is NEVER repeated by the library,
815///   because a repeated place could create a second order. Look at the order first.
816/// - Shutdown does not cancel orders at the exchange.
817///
818/// # Examples
819///
820/// The whole path against the BCS connector on its mock: reconcile, follow the order stream, place,
821/// read the state, cancel.
822///
823/// ```
824/// use std::time::Duration;
825/// use brokapi_bcs::limits::default_limits;
826/// use brokapi_bcs::{UserDataConfig, UserDataConnector};
827/// use brokapi_core::{
828///     ClientOrderId, Event, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderState,
829///     OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side,
830/// };
831/// use brokapi_runtime::BrokerLimiter;
832/// # use brokapi_bcs_protocol::config::BcsConfig;
833///
834/// # #[tokio::main(flavor = "current_thread")]
835/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
836/// # let mock_options = std::sync::Arc::new(brokapi_mocks::bcs::MockOptions::default());
837/// # let server = brokapi_mocks::bcs::MockServer::start(mock_options.clone()).await;
838/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
839/// let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
840/// let limiter = BrokerLimiter::new(&default_limits());
841/// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
842///
843/// // 1. Commands wait for this. We have no ledger yet, so we pass an empty one.
844/// let report = ud.reconcile(&[]).await?;
845/// assert!(report.confirmed.is_empty() && report.foreign.is_empty());
846///
847/// // 2. Subscribe BEFORE placing, so no event of our order is missed.
848/// let _sub = ud.subscribe_order_state().await?; // returns when the stream is open
849///
850/// // 3. Place a limit order far below the market, so it stays working.
851/// let cid = ClientOrderId::new_v4();
852/// let ack = ud
853///     .place(PlaceOrderRequest {
854///         cid,
855///         instrument: InstrumentId::new(
856///             InstrumentKey::new("SBER", "TQBR"),
857///             NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
858///         ),
859///         side: Side::Buy,
860///         order_type: OrderType::Limit,
861///         qty: Qty::from(1),
862///         price: Some(Price::from(100)),
863///     })
864///     .await?;
865/// assert_eq!(ack.cid, cid);
866///
867/// // 4. The stream tells about it, and a read agrees.
868/// let seen = tokio::time::timeout(Duration::from_secs(8), async {
869///     while let Some(env) = events.recv().await {
870///         if let Event::Order(o) = env.payload {
871///             if o.cid == cid {
872///                 return Some(o);
873///             }
874///         }
875///     }
876///     None
877/// })
878/// .await?
879/// .expect("an event for our order");
880/// assert!(!matches!(seen.state, OrderState::Rejected { .. }));
881/// let now = ud.order_state(cid).await?;
882/// assert!(matches!(now.state, OrderState::Acked { .. }));
883///
884/// // 5. Cancel it. Ok(()) means the broker says it is cancelled.
885/// ud.cancel(cid).await?;
886///
887/// ud.shutdown(ShutdownMode::Immediate).await;
888/// # Ok(())
889/// # }
890/// ```
891#[async_trait]
892pub trait OrderExecutor: Send + Sync {
893    /// Puts a new order and waits for the broker's decision.
894    ///
895    /// `Ok` means the broker accepted the order (it is working, not necessarily filled). The
896    /// [`OrderAck`] has the broker's id. Follow the order with [`order_state`](Self::order_state) or
897    /// the stream of [`subscribe_order_state`](Self::subscribe_order_state).
898    ///
899    /// The `cid` in `req` must be new. A connector sends a cid at most once: a second place with the
900    /// same cid is refused with [`Error::Rejected`] (BCS: code `duplicate_place`) without being sent.
901    /// This is on purpose. After a timeout the first one may have reached the broker, and a retry
902    /// could make a second order. Use a new cid for a new order, after you know what happened to the
903    /// first. The price is checked against the price step and the quantity against the lot when the
904    /// connector knows them (see `price_grid` in the config of the connector).
905    ///
906    /// **Dropping the future does not cancel the order.** Once the cid is marked and the request is on
907    /// its way, the connector (T-Bank, Finam) finishes the command in a task of its own: the request, the
908    /// answer and the update of the order's state happen even if you stop waiting (a `timeout` around the
909    /// call, a lost `select!`). Before that moment (the wait for the quota or for earlier commands on the
910    /// same instrument) dropping the future sends nothing. So after you drop a `place` look at the order,
911    /// as after [`Error::Unknown`].
912    ///
913    /// # Contract
914    ///
915    /// - **When:** after the first successful [`reconcile`](Self::reconcile), and not while the
916    ///   connector is degraded or shutting down. Write `req.cid` to your journal before the call.
917    /// - **Guarantee:** at most one request with this cid ever leaves the process, whatever happens.
918    /// - **Nothing sent:** the gate errors, `Config`, a local `RateLimited` and the input checks
919    ///   (`missing_price`, `price_off_step`, a wrong quantity) are raised before the request leaves.
920    ///   No order exists and the cid is unused. A new cid is always the safe choice for a retry.
921    /// - **Maybe sent:** after [`Error::Unknown`] (and, to be safe, [`Error::Transport`]) the order may
922    ///   exist. Ask [`order_state`](Self::order_state) with the same cid.
923    /// - `Ok` is the broker's acceptance, not an execution. Fills come as events.
924    ///
925    /// # Errors
926    ///
927    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is closed,
928    ///   nothing was sent. See [`reconcile`](Self::reconcile).
929    /// - [`Error::Config`] when `req.instrument.native` is of a kind this broker does not use.
930    /// - [`Error::RateLimited`] when the local limiter or the broker refuses for now. The command was
931    ///   not executed. Wait `retry_after`. The safe rule for the retry is a new cid.
932    /// - [`Error::Rejected`] when the broker (or the connector before sending) says no: a bad price
933    ///   or quantity, a limit order without a price (BCS: `missing_price`), a repeated cid
934    ///   (`duplicate_place`), a refusal inside an accepted request (BCS: `rejected`). The order does
935    ///   not exist. Change the request.
936    /// - [`Error::Unknown`] when the request may have reached the broker and there is no answer
937    ///   (timeout, cut connection, HTTP 5xx). Do NOT place again. Ask the broker with
938    ///   [`order_state`](Self::order_state) or [`active_orders`](Self::active_orders).
939    /// - [`Error::Transport`] when the failure happened before the request left, for example
940    ///   authentication. It is **not** a sign that a repeat is safe: ask
941    ///   [`Error::retryable_for_command`](crate::Error::retryable_for_command), which says `true` for
942    ///   [`Error::RateLimited`] only, and not [`Error::class`](crate::Error::class), which is the class of a read.
943    ///
944    /// # Examples
945    ///
946    /// A limit order that the mock broker refuses, and what the caller sees:
947    ///
948    /// ```
949    /// use brokapi_core::{
950    ///     ClientOrderId, Error, ErrorClass, InstrumentId, InstrumentKey, NativeId, OrderExecutor,
951    ///     OrderType, PlaceOrderRequest, Price, Qty, Side,
952    /// };
953    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
954    /// # use brokapi_core::*;
955    /// # #[tokio::main(flavor = "current_thread")]
956    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
957    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
958    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
959    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
960    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
961    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
962    /// # ud.reconcile(&[]).await?;
963    /// # let refusing_price = brokapi_conformance::REJECT_PRICE;
964    /// let sber = InstrumentId::new(
965    ///     InstrumentKey::new("SBER", "TQBR"),
966    ///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
967    /// );
968    /// let req = PlaceOrderRequest {
969    ///     cid: ClientOrderId::new_v4(),
970    ///     instrument: sber,
971    ///     side: Side::Buy,
972    ///     order_type: OrderType::Limit,
973    ///     qty: Qty::from(1),
974    ///     price: Some(refusing_price), // the mock broker refuses this price
975    /// };
976    /// match ud.place(req.clone()).await {
977    ///     Ok(ack) => println!("accepted as {}", ack.broker_id.as_str()),
978    ///     // The broker said no. The order does not exist. Fix the request and use a new cid.
979    ///     Err(e @ Error::Rejected { .. }) => assert_eq!(e.class(), ErrorClass::Fatal),
980    ///     // Maybe it exists. Never place again: ask with order_state / active_orders.
981    ///     Err(Error::Unknown) => panic!("look at the order before anything else"),
982    ///     Err(other) => panic!("unexpected: {other}"),
983    /// }
984    ///
985    /// // Sending the same cid again is refused locally: a cid is used once.
986    /// let again = ud.place(req).await.unwrap_err();
987    /// assert!(matches!(again, Error::Rejected { ref code, .. } if code == "duplicate_place"));
988    /// # ud.shutdown(ShutdownMode::Immediate).await;
989    /// # Ok(())
990    /// # }
991    /// ```
992    async fn place(&self, req: PlaceOrderRequest) -> Result<OrderAck, Error>;
993
994    /// Cancels a working order and waits for the broker to confirm.
995    ///
996    /// `Ok(())` means the broker says the order is cancelled. It cancels orders placed through
997    /// [`place`](Self::place) and [`replace`](Self::replace). For a stop order use
998    /// [`StopOrderExecutor::cancel_stop`]. A part that was filled before stays filled.
999    ///
1000    /// # Contract
1001    ///
1002    /// - **When:** while the gate is open. The order may come from an earlier run of the process: the
1003    ///   cid is enough. An order this process did not place is not queued behind other commands on its
1004    ///   instrument (the instrument is not known).
1005    /// - **Guarantee:** `Ok(())` is the broker's confirmation. A repeated cancel creates no order, so
1006    ///   it is the one command that may be repeated by hand after [`Error::Unknown`], and only
1007    ///   after [`order_state`](Self::order_state) shows the order still works.
1008    ///
1009    /// # Errors
1010    ///
1011    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is closed,
1012    ///   nothing was sent.
1013    /// - [`Error::RateLimited`]: not executed, wait `retry_after` and repeat.
1014    /// - [`Error::Rejected`] when the broker refuses: the order is unknown to it, it was filled
1015    ///   first, or the request was refused. The code depends on how the broker answers (BCS codes
1016    ///   include `not_found`, `already_filled`, `cancel_rejected`, `validation`). Read the order with
1017    ///   [`order_state`](Self::order_state) to see where it stands.
1018    /// - [`Error::Unknown`] when the answer is missing, or the answer leaves the order alive. The
1019    ///   order may still be working. Ask [`order_state`](Self::order_state) and cancel again only if
1020    ///   it is. A repeated cancel is harmless for the order, but it uses rate limit.
1021    /// - [`Error::Transport`] when the failure happened before the request left.
1022    ///
1023    /// # Examples
1024    ///
1025    /// ```
1026    /// use brokapi_core::{ClientOrderId, Error, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, Side};
1027    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1028    /// # use brokapi_core::*;
1029    /// # #[tokio::main(flavor = "current_thread")]
1030    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1031    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1032    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1033    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1034    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1035    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1036    /// # ud.reconcile(&[]).await?;
1037    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
1038    /// # let order = |price: Price| PlaceOrderRequest { cid: ClientOrderId::new_v4(), instrument: sber.clone(), side: Side::Buy, order_type: OrderType::Limit, qty: Qty::from(2), price: Some(price) };
1039    /// let req = order(Price::from(100));
1040    /// let cid = req.cid;
1041    /// ud.place(req).await?;
1042    /// ud.cancel(cid).await?;
1043    ///
1044    /// // An order that was filled before the cancel cannot be cancelled. The broker refuses, and the
1045    /// // order is not working any more.
1046    /// let req = order(Price::from(100));
1047    /// let cid = req.cid;
1048    /// ud.place(req).await?;
1049    /// # server.fill(&cid.hyphenated().to_string(), brokapi_core::Qty::from(2), brokapi_core::Price::from(100)); // the mock executes the order
1050    /// match ud.cancel(cid).await {
1051    ///     Err(Error::Rejected { code, message }) => println!("refused: {code} - {message}"),
1052    ///     other => panic!("expected a refusal, got {other:?}"),
1053    /// }
1054    /// # ud.shutdown(ShutdownMode::Immediate).await;
1055    /// # Ok(())
1056    /// # }
1057    /// ```
1058    async fn cancel(&self, cid: ClientOrderId) -> Result<(), Error>;
1059
1060    /// Changes the quantity or price of a working order.
1061    ///
1062    /// The broker cancels the original and puts a new order under `req.cid`, so the answer is a new
1063    /// order with a new broker id. The original is not active afterwards. Only where
1064    /// [`UserDataCaps::supports_replace`](crate::UserDataCaps::supports_replace) is `true`.
1065    ///
1066    /// # Contract
1067    ///
1068    /// - **When:** while the gate is open and the broker has replace
1069    ///   ([`UserDataCaps::supports_replace`](crate::UserDataCaps::supports_replace)). `req.cid` is new
1070    ///   (write it to your journal first), `req.original_cid` is the working order.
1071    /// - **Guarantee:** the same cid rules as [`place`](Self::place). After [`Error::Unknown`] ask
1072    ///   [`order_state`](Self::order_state) for **both** cids before any new command on them.
1073    ///
1074    /// # Errors
1075    ///
1076    /// - [`Error::Unsupported`] when the broker has no replace (Finam). Nothing was sent, the order
1077    ///   is untouched.
1078    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is closed.
1079    /// - [`Error::RateLimited`]: not executed, wait `retry_after`.
1080    /// - [`Error::Rejected`] when the broker refuses, for example the original is unknown or filled,
1081    ///   or (BCS) the original is partly filled and `qty` is `None` (`qty_required`).
1082    /// - [`Error::Unknown`] when the outcome is not known. The original may be replaced, still
1083    ///   working or gone. Ask [`order_state`](Self::order_state) for both cids. Do not replace again
1084    ///   until you know.
1085    /// - [`Error::Transport`] when the failure happened before the request left.
1086    ///
1087    /// # Examples
1088    ///
1089    /// ```
1090    /// use brokapi_core::{ClientOrderId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ReplaceOrderRequest, Side};
1091    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1092    /// # use brokapi_core::*;
1093    /// # #[tokio::main(flavor = "current_thread")]
1094    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1095    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1096    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1097    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1098    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1099    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1100    /// # ud.reconcile(&[]).await?;
1101    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
1102    /// assert!(ud.caps().supports_replace);
1103    /// let first = ClientOrderId::new_v4();
1104    /// ud.place(PlaceOrderRequest {
1105    ///     cid: first, instrument: sber, side: Side::Buy, order_type: OrderType::Limit,
1106    ///     qty: Qty::from(2), price: Some(Price::from(100)),
1107    /// }).await?;
1108    ///
1109    /// // Move the price. The new order has a NEW cid, the old cid is not active any more.
1110    /// let second = ClientOrderId::new_v4();
1111    /// let ack = ud.replace(ReplaceOrderRequest {
1112    ///     cid: second, original_cid: first, qty: None, price: Some(Price::from(101)),
1113    /// }).await?;
1114    /// assert_eq!(ack.cid, second);
1115    /// let active = ud.active_orders().await?;
1116    /// assert!(active.iter().any(|o| o.cid == second));
1117    /// assert!(!active.iter().any(|o| o.cid == first));
1118    /// ud.cancel(second).await?;
1119    /// # ud.shutdown(ShutdownMode::Immediate).await;
1120    /// # Ok(())
1121    /// # }
1122    /// ```
1123    async fn replace(&self, req: ReplaceOrderRequest) -> Result<OrderAck, Error>;
1124
1125    /// Reads one order by its cid, as the broker shows it now.
1126    ///
1127    /// This is how you leave the doubt of [`Error::Unknown`]. A read is not behind the trading
1128    /// gate, it works before the first reconcile. It also finds orders that are no longer active
1129    /// (filled, cancelled), unlike [`active_orders`](Self::active_orders).
1130    ///
1131    /// # Contract
1132    ///
1133    /// - **When:** at any time, also before the first [`reconcile`](Self::reconcile) and while the
1134    ///   trading gate is closed: a read is not gated.
1135    /// - **Guarantee:** reads only, so it may be repeated as often as the rate limit allows. It
1136    ///   answers for final orders too. A cid the broker does not know is an error (see below), and
1137    ///   right after an [`Error::Unknown`] that is not final: the request may still be on its way.
1138    ///
1139    /// # Errors
1140    ///
1141    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
1142    /// - [`Error::Rejected`] with code `not_found` (T-Bank: `NOT_FOUND`, compare without regard to
1143    ///   case) when the broker does not know the cid. The same at all brokers. After an
1144    ///   [`Error::Unknown`] this can mean that the order never reached the broker, or has not reached
1145    ///   it yet. There is no separate variant for it.
1146    ///
1147    /// # Examples
1148    ///
1149    /// ```
1150    /// use brokapi_core::{ClientOrderId, OrderExecutor, OrderState, OrderType, PlaceOrderRequest, Price, Qty, Side};
1151    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1152    /// # use brokapi_core::*;
1153    /// # #[tokio::main(flavor = "current_thread")]
1154    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1155    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1156    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1157    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1158    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1159    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1160    /// # ud.reconcile(&[]).await?;
1161    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
1162    /// let cid = ClientOrderId::new_v4();
1163    /// ud.place(PlaceOrderRequest {
1164    ///     cid, instrument: sber, side: Side::Buy, order_type: OrderType::Limit,
1165    ///     qty: Qty::from(2), price: Some(Price::from(100)),
1166    /// }).await?;
1167    ///
1168    /// let snapshot = ud.order_state(cid).await?;
1169    /// match snapshot.state {
1170    ///     OrderState::Acked { .. } => println!("working, nothing filled"),
1171    ///     OrderState::PartiallyFilled { filled, left, .. } => println!("{} done, {} to go", filled.decimal(), left.decimal()),
1172    ///     OrderState::Filled { .. } | OrderState::Cancelled { .. } | OrderState::Rejected { .. } => println!("final"),
1173    ///     // InFlight, Unknown, and a state that a later version adds: ask again a bit later.
1174    ///     _ => println!("ask again a bit later"),
1175    /// }
1176    /// assert_eq!(snapshot.qty, Qty::from(2));
1177    /// # ud.cancel(cid).await?;
1178    /// # ud.shutdown(ShutdownMode::Immediate).await;
1179    /// # Ok(())
1180    /// # }
1181    /// ```
1182    async fn order_state(&self, cid: ClientOrderId) -> Result<OrderSnapshot, Error>;
1183
1184    /// Lists the working orders of the account.
1185    ///
1186    /// Plain orders only: stop orders are in [`StopOrderExecutor::active_stop_orders`]. The list
1187    /// includes orders that were not placed through this connector (by hand, by another program).
1188    /// Their cid is not yours: where the broker does not give a cid back, the BCS connector makes a
1189    /// stable one from the broker id, the same for the same order every time.
1190    ///
1191    /// # Contract
1192    ///
1193    /// - **When:** at any time, the trading gate does not matter. Reads only.
1194    /// - **Guarantee:** the list is what the broker shows now. Orders of others are in it too: do not
1195    ///   touch what is not in your journal.
1196    ///
1197    /// # Errors
1198    ///
1199    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
1200    ///
1201    /// # Examples
1202    ///
1203    /// ```
1204    /// use brokapi_core::{ClientOrderId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, Side};
1205    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1206    /// # use brokapi_core::*;
1207    /// # #[tokio::main(flavor = "current_thread")]
1208    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1209    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1210    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1211    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1212    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1213    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1214    /// # ud.reconcile(&[]).await?;
1215    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
1216    /// let cid = ClientOrderId::new_v4();
1217    /// ud.place(PlaceOrderRequest {
1218    ///     cid, instrument: sber, side: Side::Buy, order_type: OrderType::Limit,
1219    ///     qty: Qty::from(3), price: Some(Price::from(100)),
1220    /// }).await?;
1221    ///
1222    /// let active = ud.active_orders().await?;
1223    /// let mine = active.iter().find(|o| o.cid == cid).expect("our order is working");
1224    /// assert_eq!(mine.price, Some(Price::from(100)));
1225    /// # ud.cancel(cid).await?;
1226    /// # ud.shutdown(ShutdownMode::Immediate).await;
1227    /// # Ok(())
1228    /// # }
1229    /// ```
1230    async fn active_orders(&self) -> Result<Vec<OrderSnapshot>, Error>;
1231
1232    /// Starts the stream of changes of your orders.
1233    ///
1234    /// Every change (accepted, partly filled, filled, cancelled, refused) arrives as
1235    /// [`Event::Order`](crate::Event::Order), tagged with the returned id, and carries the cid.
1236    ///
1237    /// The stream is [`Backpressure::Lossless`](crate::Backpressure::Lossless) with a buffer size set
1238    /// in the connector config. If you read too slowly and the buffer fills, an
1239    /// [`Event::Overflow`](crate::Event::Overflow) is sent and trading is refused with
1240    /// [`Error::Degraded`] until [`reconcile`](Self::reconcile). Keep draining your channel.
1241    ///
1242    /// Subscribe BEFORE you place. The call returns when the stream is open (see **Contract**), so an
1243    /// order placed right after it cannot be missed. Changes that happened before the call are not
1244    /// delivered: to catch up, read [`order_state`](Self::order_state) or
1245    /// [`active_orders`](Self::active_orders). There is no way to end this subscription except
1246    /// shutting the connector down.
1247    ///
1248    /// # Contract
1249    ///
1250    /// - **When:** once per connector, before [`reconcile`](Self::reconcile) and before the first
1251    ///   [`place`](Self::place). Allowed while the gate is closed.
1252    /// - **Guarantee:** the call returns only when the stream is open at the broker. The broker's own
1253    ///   confirmation where the protocol has one (T-Bank, ALOR), else the open connection of the stream
1254    ///   (BCS). Everything that happens to your orders after the return reaches you. A stream that is
1255    ///   not open within `subscribe_ack_timeout` (3 seconds by default) is [`Error::Transport`] and nothing
1256    ///   is left half open, so the call can be repeated. **Finam is the exception**: it is gRPC and may
1257    ///   send the headers of a quiet stream only with its first message, so after the wait the call
1258    ///   returns `Ok` and the stream goes on opening in the background, with no guarantee until it is
1259    ///   open (`sub_status` of the connector shows it).
1260    /// - **Guarantee:** every change of your orders arrives, in order, or the loss is announced with
1261    ///   [`Event::Overflow`](crate::Event::Overflow) and trading stops until a reconcile. The
1262    ///   subscription cannot be ended before shutdown.
1263    ///
1264    /// # Errors
1265    ///
1266    /// - [`Error::Unsupported`] when the connector has no order stream.
1267    /// - [`Error::Transport`] when the subscription could not be made, also when the stream did not open
1268    ///   in time.
1269    ///
1270    /// # Examples
1271    ///
1272    /// See the example of the trait, which follows an order on the stream.
1273    async fn subscribe_order_state(&self) -> Result<SubscriptionId, Error>;
1274
1275    /// Compares your ledger with the broker and opens the trading gate.
1276    ///
1277    /// Call it once after the connector starts, and again after [`Error::Degraded`]. A successful
1278    /// call is what lifts [`Error::NotReconciled`] and [`Error::Degraded`].
1279    ///
1280    /// `ledger` is the list of cids of orders you believe are working. Pass `&[]` if you have none.
1281    /// The report sorts the world into three:
1282    /// - `confirmed`: in your ledger and active at the broker.
1283    /// - `closed_offline`: in your ledger and NOT active at the broker (filled, cancelled, refused,
1284    ///   or never received). The report does not say which. Ask [`order_state`](Self::order_state)
1285    ///   for each, and the executions for the fills.
1286    /// - `foreign`: active at the broker and not in your ledger. Leave them alone.
1287    ///
1288    /// The comparison is with [`active_orders`](Self::active_orders), which lists plain orders.
1289    /// Keep the cids of stop orders out of the ledger and check them with
1290    /// [`StopOrderExecutor::active_stop_orders`] (not verified against the live API for every
1291    /// connector).
1292    ///
1293    /// # Contract
1294    ///
1295    /// - **When:** after [`subscribe_order_state`](Self::subscribe_order_state) at start, again after
1296    ///   [`Error::Degraded`] (read your channel to the end first), and whenever you doubt the ledger. It
1297    ///   only reads, so any number of calls is safe.
1298    /// - **Guarantee:** on `Ok` the gate is open (lifts [`Error::NotReconciled`] and
1299    ///   [`Error::Degraded`]). On an error the gate stays as it was and nothing is known about
1300    ///   your orders. Orders are never moved or cancelled.
1301    ///
1302    /// # Errors
1303    ///
1304    /// - [`Error::RateLimited`], [`Error::Transport`] when the broker could not be read. The gate
1305    ///   stays closed. Repeat later. A cid the broker answers "not found" for is not an error, it
1306    ///   goes to `closed_offline`.
1307    ///
1308    /// # Examples
1309    ///
1310    /// ```
1311    /// use brokapi_core::{ClientOrderId, Error, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, Side};
1312    /// use brokapi_bcs::{UserDataConfig, UserDataConnector};
1313    /// # use brokapi_core::*;
1314    /// # #[tokio::main(flavor = "current_thread")]
1315    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1316    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1317    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1318    /// # let limiter = || brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1319    /// # let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() });
1320    /// # let order = || PlaceOrderRequest { cid: ClientOrderId::new_v4(), instrument: sber.clone(), side: Side::Buy, order_type: OrderType::Limit, qty: Qty::from(1), price: Some(Price::from(100)) };
1321    /// # // The previous run of the program: it put an order and stopped. The order stays at the exchange.
1322    /// # let previous = UserDataConnector::start(UserDataConfig::new(cfg.clone(), "previous-run"), limiter(), tokio::sync::mpsc::channel(8).0).await?;
1323    /// # previous.reconcile(&[]).await?;
1324    /// # let left_over = order();
1325    /// # previous.place(left_over.clone()).await?;
1326    /// # previous.shutdown(ShutdownMode::Immediate).await;
1327    /// # let saved_ledger = [left_over.cid];
1328    /// let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1329    /// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter(), events_tx).await?;
1330    ///
1331    /// // Before the first reconcile, trading commands are refused and nothing is sent.
1332    /// assert!(matches!(ud.place(order()).await, Err(Error::NotReconciled)));
1333    ///
1334    /// // Our ledger has the cid saved by the previous run, and one the broker never heard of.
1335    /// let lost = ClientOrderId::new_v4();
1336    /// let ledger = [saved_ledger[0], lost];
1337    /// let report = ud.reconcile(&ledger).await?;
1338    /// assert_eq!(report.confirmed.len(), 1); // still working at the broker
1339    /// assert_eq!(report.confirmed[0].cid, saved_ledger[0]);
1340    /// assert_eq!(report.closed_offline, vec![lost]); // not active at the broker
1341    ///
1342    /// // The gate is open now.
1343    /// let next = order();
1344    /// let cid = next.cid;
1345    /// ud.place(next).await?;
1346    /// ud.cancel(cid).await?;
1347    /// # ud.cancel(saved_ledger[0]).await?;
1348    /// # ud.shutdown(ShutdownMode::Immediate).await;
1349    /// # Ok(())
1350    /// # }
1351    /// ```
1352    async fn reconcile(&self, ledger: &[ClientOrderId]) -> Result<ReconcileReport, Error>;
1353}
1354
1355/// Your own fills: the history and the live stream.
1356///
1357/// An [`Execution`] is one fill of one of your orders. Public deals of the market are in
1358/// [`MarketDataSource`]. The stream is
1359/// [`Backpressure::Lossless`](crate::Backpressure::Lossless): a fill must not be lost, so an
1360/// overflow is announced with [`Event::Overflow`](crate::Event::Overflow) and trading stops until
1361/// [`OrderExecutor::reconcile`].
1362///
1363/// # Examples
1364///
1365/// ```
1366/// use std::time::Duration;
1367/// use brokapi_bcs::limits::default_limits;
1368/// use brokapi_bcs::{UserDataConfig, UserDataConnector};
1369/// use brokapi_core::{
1370///     ClientOrderId, Event, ExecutionSource, InstrumentId, InstrumentKey, NativeId, OrderExecutor,
1371///     OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side, TimeRange, UnixNanos,
1372/// };
1373/// use brokapi_runtime::BrokerLimiter;
1374/// # use brokapi_bcs_protocol::config::BcsConfig;
1375///
1376/// # #[tokio::main(flavor = "current_thread")]
1377/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1378/// # let mock_options = std::sync::Arc::new(brokapi_mocks::bcs::MockOptions::default());
1379/// # let server = brokapi_mocks::bcs::MockServer::start(mock_options.clone()).await;
1380/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1381/// let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
1382/// let limiter = BrokerLimiter::new(&default_limits());
1383/// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1384/// ud.reconcile(&[]).await?;
1385///
1386/// // Follow the fills as they happen.
1387/// let sub = ud.subscribe_executions().await?;
1388///
1389/// let cid = ClientOrderId::new_v4();
1390/// ud.place(PlaceOrderRequest {
1391///     cid,
1392///     instrument: InstrumentId::new(
1393///         InstrumentKey::new("SBER", "TQBR"),
1394///         NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
1395///     ),
1396///     side: Side::Buy,
1397///     order_type: OrderType::Limit,
1398///     qty: Qty::from(2),
1399///     price: Some(Price::from(100)),
1400/// })
1401/// .await?;
1402/// # server.fill(&cid.hyphenated().to_string(), brokapi_core::Qty::from(2), brokapi_core::Price::from(100)); // the mock executes the order
1403///
1404/// let fill = tokio::time::timeout(Duration::from_secs(8), async {
1405///     while let Some(env) = events.recv().await {
1406///         if let Event::Execution(x) = env.payload {
1407///             assert_eq!(env.sub, Some(sub));
1408///             return Some(x);
1409///         }
1410///     }
1411///     None
1412/// })
1413/// .await?
1414/// .expect("a fill arrived");
1415/// assert_eq!(fill.cid, Some(cid));
1416/// assert!(fill.qty.is_positive());
1417///
1418/// // The history of the day, the same fill with its trade id.
1419/// let everything = TimeRange::new(UnixNanos::ZERO, UnixNanos::from_nanos(i64::MAX / 2));
1420/// let history = ud.executions(everything).await?;
1421/// assert!(history.iter().any(|x| x.trade_id.len() > 0));
1422///
1423/// ud.shutdown(ShutdownMode::Immediate).await;
1424/// # Ok(())
1425/// # }
1426/// ```
1427#[async_trait]
1428pub trait ExecutionSource: Send + Sync {
1429    /// Reads your fills in `range`.
1430    ///
1431    /// Use it to find what happened to an order after [`Error::Unknown`], or to catch up after a
1432    /// gap. `cid` of an [`Execution`] is `None` when the broker does not echo it (the BCS history
1433    /// does not), match by `broker_order_id` then. `commission` is `0.0` where the broker gives none
1434    /// (BCS history). How far back it goes is decided by the broker.
1435    ///
1436    /// # Contract
1437    ///
1438    /// - **When:** at any time. Reads only. The way to catch up on fills after [`Error::Unknown`], an
1439    ///   [`Event::Overflow`](crate::Event::Overflow) (from its `lost_from`) or a restart.
1440    ///
1441    /// # Errors
1442    ///
1443    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
1444    /// - [`Error::Rejected`] when the broker refuses the request.
1445    ///
1446    /// # Examples
1447    ///
1448    /// ```
1449    /// use brokapi_core::{ExecutionSource, TimeRange, UnixNanos};
1450    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1451    /// # use brokapi_core::*;
1452    /// # #[tokio::main(flavor = "current_thread")]
1453    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1454    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1455    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1456    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1457    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1458    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1459    /// let everything = TimeRange::new(UnixNanos::ZERO, UnixNanos::from_nanos(i64::MAX / 2));
1460    /// let history = ud.executions(everything).await?;
1461    /// // A fresh account has no fills. The call still works.
1462    /// assert!(history.is_empty());
1463    /// # ud.shutdown(ShutdownMode::Immediate).await;
1464    /// # Ok(())
1465    /// # }
1466    /// ```
1467    async fn executions(&self, range: TimeRange) -> Result<Vec<Execution>, Error>;
1468
1469    /// Starts the stream of your fills.
1470    ///
1471    /// Each fill arrives once as [`Event::Execution`](crate::Event::Execution), tagged with the
1472    /// returned id. The stream is lossless and has a buffer size from the connector config: keep
1473    /// draining your channel. Subscribe before you place: the call returns when the stream is open, so a
1474    /// fill after it cannot be missed, but fills that happened before are not delivered (read
1475    /// [`executions`](Self::executions) to catch up). There is no way to end this subscription except
1476    /// shutting the connector down.
1477    ///
1478    /// # Contract
1479    ///
1480    /// - **When:** once per connector, before the first [`OrderExecutor::place`]. Allowed while the gate
1481    ///   is closed.
1482    /// - **Guarantee:** returns only when the stream is open, like
1483    ///   [`OrderExecutor::subscribe_order_state`]. Lossless like it: a loss is announced with
1484    ///   [`Event::Overflow`](crate::Event::Overflow).
1485    ///
1486    /// # Errors
1487    ///
1488    /// - [`Error::Unsupported`] when the broker has no fill stream
1489    ///   ([`UserDataCaps::streams_executions`](crate::UserDataCaps::streams_executions) is `false`).
1490    ///   Poll [`executions`](Self::executions) yourself then.
1491    /// - [`Error::Transport`] when the subscription could not be made, also when the stream did not open
1492    ///   in time.
1493    ///
1494    /// # Examples
1495    ///
1496    /// See the example of the trait, which receives a fill.
1497    async fn subscribe_executions(&self) -> Result<SubscriptionId, Error>;
1498}
1499
1500/// The account: money and positions, as a snapshot and as a stream.
1501///
1502/// # Examples
1503///
1504/// ```
1505/// use std::time::Duration;
1506/// use brokapi_bcs::limits::default_limits;
1507/// use brokapi_bcs::{UserDataConfig, UserDataConnector};
1508/// use brokapi_core::{Event, PortfolioSource, ShutdownMode, SubscribeOpts};
1509/// use brokapi_runtime::BrokerLimiter;
1510/// # use brokapi_bcs_protocol::config::BcsConfig;
1511///
1512/// # #[tokio::main(flavor = "current_thread")]
1513/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1514/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1515/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base);
1516/// let (events_tx, mut events) = tokio::sync::mpsc::channel(256);
1517/// let limiter = BrokerLimiter::new(&default_limits());
1518/// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1519///
1520/// // A stream: each event is a full snapshot, so the latest one wins if we read slowly.
1521/// // Subscribe first: an update that arrives before the subscription belongs to no subscription.
1522/// let sub = ud.subscribe_portfolio(SubscribeOpts::conflate()).await?;
1523/// let update = tokio::time::timeout(Duration::from_secs(8), async {
1524///     while let Some(env) = events.recv().await {
1525///         // The channel is shared by all subscriptions of the connector: pick ours by `sub`.
1526///         if env.sub == Some(sub) {
1527///             return Some(env.payload);
1528///         }
1529///     }
1530///     None
1531/// })
1532/// .await?
1533/// .expect("an update arrived");
1534/// assert!(matches!(
1535///     update,
1536///     Event::Portfolio(_) | Event::Positions(_) | Event::Balances(_)
1537/// ));
1538///
1539/// // The whole account at once, or only one part of it.
1540/// let portfolio = ud.portfolio().await?;
1541/// let balances = ud.balances().await?;
1542/// let positions = ud.positions().await?;
1543/// println!("{} currencies, {} positions", balances.len(), positions.len());
1544/// assert_eq!(portfolio.positions.len(), positions.len());
1545///
1546/// ud.shutdown(ShutdownMode::Immediate).await;
1547/// # Ok(())
1548/// # }
1549/// ```
1550#[async_trait]
1551pub trait PortfolioSource: Send + Sync {
1552    /// Reads money and positions at one moment.
1553    ///
1554    /// # Errors
1555    ///
1556    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
1557    /// - [`Error::Rejected`] when the broker refuses the request.
1558    ///
1559    /// # Examples
1560    ///
1561    /// ```
1562    /// use brokapi_core::PortfolioSource;
1563    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1564    /// # use brokapi_core::*;
1565    /// # #[tokio::main(flavor = "current_thread")]
1566    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1567    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1568    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
1569    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1570    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1571    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1572    /// let p = ud.portfolio().await?;
1573    /// for b in &p.balances {
1574    ///     assert!(!b.available.is_negative() && !b.blocked.is_negative());
1575    /// }
1576    /// # ud.shutdown(ShutdownMode::Immediate).await;
1577    /// # Ok(())
1578    /// # }
1579    /// ```
1580    async fn portfolio(&self) -> Result<Portfolio, Error>;
1581
1582    /// Reads the positions only.
1583    ///
1584    /// The BCS connector reads the whole portfolio and returns this part, so it costs the same as
1585    /// [`portfolio`](Self::portfolio).
1586    ///
1587    /// # Errors
1588    ///
1589    /// The same as [`portfolio`](Self::portfolio).
1590    ///
1591    /// # Examples
1592    ///
1593    /// ```
1594    /// use brokapi_core::PortfolioSource;
1595    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1596    /// # use brokapi_core::*;
1597    /// # #[tokio::main(flavor = "current_thread")]
1598    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1599    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1600    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
1601    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1602    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1603    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1604    /// for p in ud.positions().await? {
1605    ///     println!("{}: {}", p.instrument.key.display(), p.qty);
1606    /// }
1607    /// # ud.shutdown(ShutdownMode::Immediate).await;
1608    /// # Ok(())
1609    /// # }
1610    /// ```
1611    async fn positions(&self) -> Result<Vec<Position>, Error>;
1612
1613    /// Reads the money balances only.
1614    ///
1615    /// The BCS connector reads the whole portfolio and returns this part, so it costs the same as
1616    /// [`portfolio`](Self::portfolio).
1617    ///
1618    /// # Errors
1619    ///
1620    /// The same as [`portfolio`](Self::portfolio).
1621    ///
1622    /// # Examples
1623    ///
1624    /// ```
1625    /// use brokapi_core::PortfolioSource;
1626    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1627    /// # use brokapi_core::*;
1628    /// # #[tokio::main(flavor = "current_thread")]
1629    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1630    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1631    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base);
1632    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1633    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1634    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1635    /// let free_money: Money = ud.balances().await?.iter().map(|b| b.available).sum();
1636    /// assert!(!free_money.is_negative());
1637    /// # ud.shutdown(ShutdownMode::Immediate).await;
1638    /// # Ok(())
1639    /// # }
1640    /// ```
1641    async fn balances(&self) -> Result<Vec<Balance>, Error>;
1642
1643    /// Starts a stream of portfolio updates.
1644    ///
1645    /// Updates arrive as [`Event::Portfolio`](crate::Event::Portfolio), or as
1646    /// [`Event::Positions`](crate::Event::Positions) and [`Event::Balances`](crate::Event::Balances)
1647    /// where the connector sends the parts separately (the T-Bank connector does). Each is a full
1648    /// snapshot of its part and replaces the previous one, so
1649    /// [`Backpressure::Conflate`](crate::Backpressure::Conflate) fits.
1650    ///
1651    /// The broker may push a first snapshot as soon as its portfolio channel connects, which can be
1652    /// before you subscribe. Such an update has no subscription (`Envelope::sub` is `None`). Subscribe
1653    /// right after the connector starts and then read [`portfolio`](Self::portfolio) once for a known
1654    /// starting point.
1655    ///
1656    /// # Errors
1657    ///
1658    /// - [`Error::Unsupported`] when the connector has no portfolio stream.
1659    /// - [`Error::Transport`] when the subscription could not be made, also when the stream did not open
1660    ///   in time (the call returns when the stream is open, as `subscribe_order_state`).
1661    ///
1662    /// # Examples
1663    ///
1664    /// See the example of the trait, which receives an update.
1665    async fn subscribe_portfolio(&self, opts: SubscribeOpts) -> Result<SubscriptionId, Error>;
1666}
1667
1668/// Stop orders, take-profit and stop-limit - an extra capability next to [`OrderExecutor`]
1669/// ([`UserDataCaps::supports_stop_orders`](crate::UserDataCaps::supports_stop_orders) tells whether the broker has it).
1670///
1671/// A connector without stop orders implements the trait and answers `Error::Unsupported` to every
1672/// method. The command gate of the connector (`NotReconciled`, `Degraded`, shutdown) applies to
1673/// `place_stop` and `cancel_stop` like to `place` and `cancel`, and a stop order that got no answer
1674/// is `Error::Unknown`, never repeated by the library. A stop order waits at the broker and is not
1675/// an order on the exchange until the price crosses the trigger, see [`StopOrderKind`](crate::StopOrderKind).
1676///
1677/// # Examples
1678///
1679/// ```
1680/// use brokapi_bcs::limits::default_limits;
1681/// use brokapi_bcs::{UserDataConfig, UserDataConnector};
1682/// use brokapi_core::{
1683///     ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor,
1684///     PlaceStopOrderRequest, Price, Qty, ShutdownMode, Side, StopExpiry, StopOrderExecutor,
1685///     StopOrderKind, StopOrderRef, StopOrderState,
1686/// };
1687/// use brokapi_runtime::BrokerLimiter;
1688/// # use brokapi_bcs_protocol::config::BcsConfig;
1689///
1690/// # #[tokio::main(flavor = "current_thread")]
1691/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1692/// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1693/// # let cfg = BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1694/// let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1695/// let limiter = BrokerLimiter::new(&default_limits());
1696/// let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1697/// ud.reconcile(&[]).await?; // stop orders are behind the same gate as plain orders
1698/// assert!(ud.caps().supports_stop_orders);
1699///
1700/// let sber = InstrumentId::new(
1701///     InstrumentKey::new("SBER", "TQBR"),
1702///     NativeId::TickerClass { ticker: "SBER".into(), class_code: "TQBR".into() },
1703/// );
1704///
1705/// // A stop-loss: sell 1 if the price falls to 200, far below the market so it only waits.
1706/// let cid = ClientOrderId::new_v4();
1707/// let ack = ud
1708///     .place_stop(PlaceStopOrderRequest {
1709///         cid,
1710///         instrument: sber.clone(),
1711///         side: Side::Sell,
1712///         kind: StopOrderKind::StopLoss,
1713///         qty: Qty::from(1),
1714///         stop_price: Price::from(200),
1715///         limit_price: None, // market order at the trigger
1716///         expiry: StopExpiry::GoodTillCancel,
1717///     })
1718///     .await?;
1719///
1720/// // It is listed with its parameters while it waits.
1721/// let waiting = ud.active_stop_orders().await?;
1722/// let mine = waiting.iter().find(|s| s.broker_id == ack.broker_id).expect("listed");
1723/// assert_eq!(mine.state, StopOrderState::Active);
1724///
1725/// // Cancel it by the cid we gave. `StopOrderRef::Broker` names it by the broker's id instead.
1726/// ud.cancel_stop(StopOrderRef::Cid(cid)).await?;
1727///
1728/// // A stop-limit needs a limit price. Without it the request is refused before anything is sent.
1729/// let bad = PlaceStopOrderRequest {
1730///     cid: ClientOrderId::new_v4(),
1731///     instrument: sber,
1732///     side: Side::Sell,
1733///     kind: StopOrderKind::StopLimit,
1734///     qty: Qty::from(1),
1735///     stop_price: Price::from(200),
1736///     limit_price: None,
1737///     expiry: StopExpiry::GoodTillCancel,
1738/// };
1739/// assert!(matches!(ud.place_stop(bad).await, Err(Error::Rejected { .. })));
1740///
1741/// ud.shutdown(ShutdownMode::Immediate).await;
1742/// # Ok(())
1743/// # }
1744/// ```
1745#[async_trait]
1746pub trait StopOrderExecutor: Send + Sync {
1747    /// Puts a stop order and waits for the broker's decision.
1748    ///
1749    /// The `cid` is the same kind of UUID as for plain orders and follows the same rule: it is sent
1750    /// at most once, a repeat is refused with [`Error::Rejected`] (BCS: `duplicate_place`). `Ok`
1751    /// means the broker accepted the stop order, it is waiting for its trigger. The library checks
1752    /// the shape of the request (a stop-limit needs `limit_price`) and nothing else: the trigger
1753    /// direction against the market price is the broker's to judge.
1754    ///
1755    /// # Contract
1756    ///
1757    /// - **When:** after the first successful [`OrderExecutor::reconcile`], same gate as
1758    ///   [`OrderExecutor::place`]. Write `req.cid` to your journal first.
1759    /// - **Guarantee:** at most one request with this cid ever leaves the process. After
1760    ///   [`Error::Unknown`] look in [`active_stop_orders`](Self::active_stop_orders), do not place again.
1761    ///
1762    /// # Errors
1763    ///
1764    /// - [`Error::Unsupported`] when the broker has no stop orders. Nothing was sent.
1765    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is closed,
1766    ///   nothing was sent.
1767    /// - [`Error::Rejected`] when the request is wrong or the broker says no: a stop-limit without a
1768    ///   limit price, a repeated cid, an expiry the broker does not have for this kind.
1769    /// - [`Error::RateLimited`]: not executed, wait `retry_after`. Use a new cid for the retry.
1770    /// - [`Error::Unknown`] when the request may have reached the broker and there is no answer. Do
1771    ///   NOT place again. Look in [`active_stop_orders`](Self::active_stop_orders): a stop order with
1772    ///   your cid is yours. A stop order with `cid: None` on the same instrument and side may be yours
1773    ///   too, when the connector could not tell for sure (T-Bank does not return the cid and links it
1774    ///   only when exactly one stop order fits). Then do not place another one under a new cid either.
1775    ///   Only an empty list means that nothing is at the broker (look a second time after a few seconds
1776    ///   first: a slow broker takes the order late), and then a new cid is safe.
1777    /// - [`Error::Transport`] when the failure happened before the request left.
1778    ///
1779    /// # Examples
1780    ///
1781    /// See the example of the trait.
1782    async fn place_stop(&self, req: PlaceStopOrderRequest) -> Result<OrderAck, Error>;
1783
1784    /// Cancels a stop order that has not triggered.
1785    ///
1786    /// Name it by the cid you gave ([`StopOrderRef::Cid`]) or by the broker's id
1787    /// ([`StopOrderRef::Broker`]). A cid is known from what this process has seen. After a restart a
1788    /// connector may not be able to find it, use the broker id then (from
1789    /// [`active_stop_orders`](Self::active_stop_orders)).
1790    ///
1791    /// # Contract
1792    ///
1793    /// - **When:** while the gate is open. After a restart name the stop by the broker's id.
1794    /// - **Guarantee:** a repeated cancel creates nothing. After [`Error::Unknown`] look in
1795    ///   [`active_stop_orders`](Self::active_stop_orders) first.
1796    ///
1797    /// # Errors
1798    ///
1799    /// - [`Error::Unsupported`] when the broker has no stop orders.
1800    /// - [`Error::NotReconciled`], [`Error::Degraded`], [`Error::ShuttingDown`]: the gate is closed.
1801    /// - [`Error::Rejected`] when the broker refuses: the stop order is unknown (BCS: `not_found`),
1802    ///   or it already triggered.
1803    /// - [`Error::RateLimited`]: not executed, wait `retry_after`.
1804    /// - [`Error::Unknown`] when the answer is missing or leaves the stop order alive. Look in
1805    ///   [`active_stop_orders`](Self::active_stop_orders) before you decide.
1806    /// - [`Error::Transport`] when the failure happened before the request left.
1807    ///
1808    /// # Examples
1809    ///
1810    /// See the example of the trait.
1811    async fn cancel_stop(&self, stop: StopOrderRef) -> Result<(), Error>;
1812
1813    /// Lists the stop orders that wait for their trigger.
1814    ///
1815    /// `cid` of an entry is `None` when the connector cannot tie the broker's stop order to a cid you
1816    /// gave (placed by someone else, or placed before this process started and not echoed). Use
1817    /// `broker_id` then.
1818    ///
1819    /// # Errors
1820    ///
1821    /// - [`Error::Unsupported`] when the broker has no stop orders.
1822    /// - [`Error::RateLimited`], [`Error::Transport`]: wait or repeat later.
1823    ///
1824    /// # Examples
1825    ///
1826    /// See the example of the trait.
1827    async fn active_stop_orders(&self) -> Result<Vec<StopOrderSnapshot>, Error>;
1828
1829    /// Stream of stop order statuses. `Unsupported` where the broker has no such stream.
1830    ///
1831    /// The BCS connector has none: its order events channel is documented for plain orders only.
1832    /// Poll [`active_stop_orders`](Self::active_stop_orders) instead. The library does not poll for you.
1833    ///
1834    /// # Errors
1835    ///
1836    /// - [`Error::Unsupported`] when the broker has no such stream. BCS: capability
1837    ///   `stop_order_stream`.
1838    /// - [`Error::Transport`] when the subscription could not be made.
1839    ///
1840    /// # Examples
1841    ///
1842    /// ```
1843    /// use brokapi_core::{Error, StopOrderExecutor};
1844    /// # use brokapi_bcs::{UserDataConfig, UserDataConnector};
1845    /// # use brokapi_core::*;
1846    /// # #[tokio::main(flavor = "current_thread")]
1847    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1848    /// # let server = brokapi_mocks::bcs::MockServer::start(Default::default()).await;
1849    /// # let cfg = brokapi_bcs_protocol::config::BcsConfig::for_mock("mock-refresh-token", &server.http_base).with_write(true);
1850    /// # let (events_tx, _events) = tokio::sync::mpsc::channel(256);
1851    /// # let limiter = brokapi_runtime::BrokerLimiter::new(&brokapi_bcs::limits::default_limits());
1852    /// # let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bcs-ud"), limiter, events_tx).await?;
1853    /// match ud.subscribe_stop_orders().await {
1854    ///     Err(Error::Unsupported { capability }) => assert_eq!(capability, "stop_order_stream"),
1855    ///     other => panic!("BCS has no stop order stream, got {other:?}"),
1856    /// }
1857    /// # ud.shutdown(ShutdownMode::Immediate).await;
1858    /// # Ok(())
1859    /// # }
1860    /// ```
1861    async fn subscribe_stop_orders(&self) -> Result<SubscriptionId, Error>;
1862}