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}