Struct UserDataConnector
pub struct UserDataConnector { /* private fields */ }Expand description
T-Bank user data connector: orders, own trades, portfolio and stop orders of ONE account.
It implements [OrderExecutor], [ExecutionSource], [PortfolioSource] and
[StopOrderExecutor]. Commands return their answer directly. Order states, own trades and
portfolio changes arrive as [Envelope]s in the channel given to start, after
you subscribe.
§Rules that protect your money
- The gate. Trading commands return [
Error::NotReconciled] until [OrderExecutor::reconcile] has succeeded once. Call it right afterstartwith the cids you have on record (an empty slice if none). - A place is never repeated. If a
placegets no answer (timeout, broken connection, server fault) the result is [Error::Unknown] and the library does NOT send it again, because a retry could create a second order. Acidis used for oneplaceat most: sending it again is refused locally asduplicate_place. Look at the order withorder_state, and use a NEWcidfor a new attempt. - One account. Messages for another account are dropped. For several accounts of one token start one connector per account and share ONE rate limiter between them.
- Units, not lots. Quantities are in units of the instrument (shares, bonds), as at every broker. T-Bank counts
in lots: the connector converts both ways with the lot size of the instrument (read once, from the directory). An
order that is not a whole number of lots is refused with
invalid_qtybefore it is sent. Prices are per one instrument.
§Examples
Place an order, cancel it, and see a refusal. The mock refuses a limit order at one marker price (13.37), where a real broker refuses a price outside its band.
use brokapi_core::{
ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
Price, Qty, ShutdownMode, Side,
};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(256);
let limiter = BrokerLimiter::new(&default_limits());
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud-main"), limiter, events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10), // one lot of SBER: 10 shares
price: Some(price),
};
// 1. The gate is closed until the first reconcile. Nothing was sent.
let early = ud.place(buy(ClientOrderId::new_v4(), Price::from(250))).await;
assert!(matches!(early, Err(Error::NotReconciled)));
// 2. Reconcile with the cids you know. A fresh start knows none.
let report = ud.reconcile(&[]).await?;
assert!(report.confirmed.is_empty());
// 3. Place, look, cancel.
let cid = ClientOrderId::new_v4();
let ack = ud.place(buy(cid, Price::from(250))).await?;
assert_eq!(ack.cid, cid);
assert_eq!(ud.active_orders().await?.len(), 1);
ud.cancel(cid).await?;
assert!(ud.active_orders().await?.is_empty());
// 4. A refusal is `Rejected`. The broker did nothing, and the next order is not blocked.
let refused = ud.place(buy(ClientOrderId::new_v4(), refused_price)).await;
assert!(matches!(refused, Err(Error::Rejected { .. })));
assert!(ud.place(buy(ClientOrderId::new_v4(), Price::from(251))).await.is_ok());
ud.shutdown(ShutdownMode::Immediate).await;§Several accounts
One token can see several accounts. Start one connector per account. Give each its own source
and share ONE limiter, because T-Bank counts its limits per user.
use std::sync::Arc;
use brokapi_core::{OrderExecutor, ShutdownMode};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use brokapi_tbank_protocol::TbankClient;
use tokio::sync::mpsc;
// `token_config` is your sandbox or production config without an account.
let accounts = TbankClient::new(&token_config)?.list_accounts().await?;
let limiter = BrokerLimiter::new(&default_limits()); // ONE limiter for all accounts
let mut connectors = Vec::new();
for account in &accounts {
let (events_tx, _events) = mpsc::channel(256);
let cfg = UserDataConfig::new(
token_config.clone().with_account(account.id.clone()),
format!("tbank-ud-{}", account.id),
);
let ud = UserDataConnector::start(cfg, Arc::clone(&limiter), events_tx).await?;
ud.reconcile(&[]).await?;
connectors.push(ud);
}
assert_eq!(connectors.len(), accounts.len());
assert_ne!(connectors[0].account_id(), connectors[1].account_id());
for ud in connectors {
ud.shutdown(ShutdownMode::Immediate).await;
}Implementations§
§impl UserDataConnector
impl UserDataConnector
pub fn in_flight_orders(&self) -> usize
pub fn in_flight_orders(&self) -> usize
How many commands are in flight: sent, and the answer has not been taken yet. Zero when all of them
are settled. A place runs to its end even if the caller drops its future, so this number goes back
to zero by itself. For health checks and tests.
§Examples
use brokapi_core::ShutdownMode;
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
let (events_tx, _events) = tokio::sync::mpsc::channel(16);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
assert_eq!(ud.in_flight_orders(), 0);
ud.shutdown(ShutdownMode::Immediate).await;pub async fn start(
cfg: UserDataConfig,
limiter: Arc<BrokerLimiter>,
out: Sender<Envelope>,
) -> Result<UserDataConnector, Error>
pub async fn start( cfg: UserDataConfig, limiter: Arc<BrokerLimiter>, out: Sender<Envelope>, ) -> Result<UserDataConnector, Error>
Resolves the account (cfg.tbank.account_id, or the token’s only open account) and starts.
Commands are refused with NotReconciled until [OrderExecutor::reconcile] succeeds.
With an account_id in the config nothing is sent at start. Without one the connector calls
GetAccounts once, and takes the only open account. Streams are opened later, when you
subscribe. limiter should be the one limiter of the whole user (see crate::limits).
§Errors
- [
Error::Config] when the token or endpoint is invalid, when noaccount_idis set and the token has no open account, when it has several (settbank.account_id), or when the broker does not accept the token. - [
Error::Transport] when theGetAccountscall fails on the network (only without anaccount_id), and [Error::RateLimited] when it is refused for rate.
§Examples
use brokapi_core::{Error, ShutdownMode};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let limiter = BrokerLimiter::new(&default_limits());
let (events_tx, _events) = mpsc::channel(256);
// This token sees two accounts, so the connector will not guess: it refuses to start.
let cfg = UserDataConfig::new(token_config.clone(), "tbank-ud");
let result = UserDataConnector::start(cfg, limiter.clone(), events_tx.clone()).await;
assert!(matches!(result, Err(Error::Config(_))));
// Name the account and it starts.
let cfg = UserDataConfig::new(token_config.with_account(ACCOUNT_A), "tbank-ud");
let ud = UserDataConnector::start(cfg, limiter, events_tx).await?;
assert_eq!(ud.account_id(), ACCOUNT_A);
ud.shutdown(ShutdownMode::Immediate).await;pub fn account_id(&self) -> &str
pub fn account_id(&self) -> &str
The brokerage account this connector works on: the configured one, or the token’s only open account.
§Examples
use brokapi_core::ShutdownMode;
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(16);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
assert_eq!(ud.account_id(), ACCOUNT_B);pub fn raw(&self) -> TbankRaw
pub fn raw(&self) -> TbankRaw
Any other method of the T-Bank API with this connector’s token, endpoint and rate limiter.
account_id() of the result is this connector’s account. See TbankRaw.
The raw layer does not know the connector’s order registry: orders placed through it are not
tracked (no events, no reconcile). Use the trait methods for orders. Use call_command
for anything that changes the account, so a lost answer is [Error::Unknown].
§Examples
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use brokapi_core::ShutdownMode;
use brokapi_tbank_protocol::v1 as pb;
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(16);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let raw = ud.raw();
assert_eq!(raw.account_id(), Some(ACCOUNT_A));
let account = ud.account_id().to_string();
// A read of the portfolio, spelled out through the raw layer.
let portfolio = raw
.call("GetPortfolio", |c| async move {
c.operations().get_portfolio(pb::PortfolioRequest { account_id: account, currency: None }).await
})
.await?;
assert!(!portfolio.positions.is_empty());pub fn price_grid(&self) -> &PriceGrid
pub fn price_grid(&self) -> &PriceGrid
The price steps this connector checks order prices against. Fill it from
TradingParams (grid.learn(¶ms)) to have a price off the
step refused before anything is sent. See [PriceGrid].
pub fn caps(&self) -> UserDataCaps
pub fn caps(&self) -> UserDataCaps
What this connector can do.
place_idempotency is LabelOnly on purpose: order_id is documented as an idempotency key
(“для целей идемпотентности”; error 30057 mentions a duplicate with a found report), but
nobody has measured the broker’s behavior on a repeated key, and the library requires LabelOnly
until it is confirmed on the sandbox (a live run). The library never repeats a
place itself regardless.
Replace and stop orders are supported, and own trades are streamed. There is no websocket RPC
and no ordered command channel: commands are plain gRPC calls, serialized per instrument by
the connector. sandbox is the sandbox flag of the config.
§Examples
use brokapi_core::{PlaceIdempotency, ShutdownMode};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(16);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let caps = ud.caps();
assert_eq!(caps.place_idempotency, PlaceIdempotency::LabelOnly);
assert!(caps.supports_replace && caps.supports_stop_orders && caps.streams_executions);
assert!(caps.sandbox, "a mock config is a sandbox config");pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport
pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport
Stops the connector. Orders already at the exchange are NOT touched.
The command gate closes first. Placements still in flight are abandoned locally: each becomes
Unknown with one event, and its cid is listed in inflight_abandoned. Look at those orders
with a new connector (reconcile, order_state) before you place anything again.
With [ShutdownMode::Graceful] the open server streams are closed (the broker has no
unsubscribe message, ending the call is the way) and unsubscribed counts these streams (a
portfolio subscription uses two). The event backlog gets until the deadline to drain into your
channel. With [ShutdownMode::Immediate] nothing is waited for and unsubscribed is 0.
unsubscribe_failed is always 0 here. The method consumes the connector.
§Examples
use std::time::Duration;
use brokapi_core::{ExecutionSource, OrderExecutor, ShutdownMode};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.subscribe_order_state().await?;
ud.subscribe_executions().await?;
let report = ud.shutdown(ShutdownMode::Graceful { deadline: Duration::from_secs(2) }).await;
assert_eq!(report.unsubscribed, 2, "two streams were open");
assert!(report.inflight_abandoned.is_empty());Trait Implementations§
§impl Connector for UserDataConnector
impl Connector for UserDataConnector
§fn caps(&self) -> UserDataCaps
fn caps(&self) -> UserDataCaps
§fn shutdown<'async_trait>(
self,
mode: ShutdownMode,
) -> Pin<Box<dyn Future<Output = ShutdownReport> + Send + 'async_trait>>where
UserDataConnector: 'async_trait,
fn shutdown<'async_trait>(
self,
mode: ShutdownMode,
) -> Pin<Box<dyn Future<Output = ShutdownReport> + Send + 'async_trait>>where
UserDataConnector: 'async_trait,
Error::Unknown], see [ShutdownReport]) and ends its tasks. Takes the connector by value, so it
cannot be used afterwards.§impl ExecutionSource for UserDataConnector
impl ExecutionSource for UserDataConnector
§fn executions<'life0, 'async_trait>(
&'life0 self,
range: TimeRange,
) -> Pin<Box<dyn Future<Output = Result<Vec<Execution>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn executions<'life0, 'async_trait>(
&'life0 self,
range: TimeRange,
) -> Pin<Box<dyn Future<Output = Result<Vec<Execution>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
History from GetOperationsByCursor (executed BUY/SELL operations, one Execution per trade).
Differences from the live stream are in map::execution::from_operation.
Quantities here are in units of the instrument, like everywhere (the broker reports the history and the stream of own trades in pieces, not in lots).
cid is None: the history has no order id. The commission of an operation is spread over its
trades in proportion to quantity. At most 20 pages of 1000 operations are read per call. A
range that holds more is an error, never a cut-off list: ask for shorter ranges.
§Errors
- [
Error::Rejected] with coderange_too_longwhen the range holds more than 20 pages of 1000 operations. Nothing is returned, so a short list never passes for the whole history. Split the range and read the parts. - [
Error::RateLimited] (the operations quota is small), [Error::Transport], [Error::Config] and [Error::Rejected], as fororder_state. A read, so repeat it later.
§Examples
use brokapi_core::{ExecutionSource, TimeRange, UnixNanos};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let range = TimeRange::new(UnixNanos::ZERO, UnixNanos::from_nanos(i64::MAX / 2));
let history = ud.executions(range).await?;
assert!(history.iter().all(|x| x.qty.is_positive()));§fn subscribe_executions<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn subscribe_executions<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Own trades as they happen (TradesStream), Lossless.
Same buffer and overflow rules as subscribe_order_state.
Quantities are in units of the instrument (the broker reports own trades in pieces) and the stream carries no commission (0.0). Calling it again returns the
same id.
§Errors
The same as subscribe_order_state.
§Examples
use brokapi_core::ExecutionSource;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let sub = ud.subscribe_executions().await?;
assert_eq!(ud.subscribe_executions().await?, sub);§impl OrderExecutor for UserDataConnector
impl OrderExecutor for UserDataConnector
§fn place<'life0, 'async_trait>(
&'life0 self,
req: PlaceOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn place<'life0, 'async_trait>(
&'life0 self,
req: PlaceOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Places an order (PostOrder). req.cid goes to the broker as order_id, its idempotency key.
Units: qty is a whole number of LOTS (at least 1), price is per one instrument. The price is
converted to units and nano. It is not rounded to the price step, the broker checks that.
A limit order needs a positive price. A market order ignores the price. Commands on one
instrument go one at a time, in the order they arrive.
Never repeated. If the call gets no clear answer, the result is [Error::Unknown] and the
library does not send it again, because a retry could create a second order. The cid is used up:
placing it again is refused locally as duplicate_place. First ask
order_state or active_orders, and place a
new order under a NEW cid only when the broker has none.
§Errors
- [
Error::NotReconciled] before the first successfulreconcile, [Error::Degraded] after an event buffer overflow, [Error::ShuttingDown] during shutdown. Nothing was sent. - [
Error::Rejected] with codeinvalid_qty(zero, a fraction, or not a whole number of lots),missing_price(limit order without a positive price) orduplicate_place(thiscidwas placed before). Nothing was sent. - [
Error::Rejected] when the broker refuses. The code is the gRPC status name (for exampleInvalidArgument) and the message is the broker’s error code. The same codeREJECTEDis used when the broker accepted the call but reports the order as rejected. The broker did nothing, and thecidstays used. - [
Error::RateLimited] from the local limiter: refused before thecidis registered, so the samecidcan be used again. If the broker itself saysResourceExhausted, thecidis used up. - [
Error::Config] when the broker does not accept the token (Unauthenticated). - [
Error::Unknown] on a timeout (request_timeout), a broken connection or a server fault. The order may or may not exist.
§Examples
use brokapi_core::{
ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
Price, Qty, ShutdownMode, Side,
};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let order = |cid, qty, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty,
price: Some(price),
};
// The normal path.
let cid = ClientOrderId::new_v4();
let ack = ud.place(order(cid, Qty::from(10), Price::from(250))).await?;
assert_eq!(ack.cid, cid);
// The same cid again is refused locally, nothing is sent.
match ud.place(order(cid, Qty::from(10), Price::from(250))).await {
Err(Error::Rejected { code, .. }) => assert_eq!(code, "duplicate_place"),
other => panic!("expected duplicate_place, got {other:?}"),
}
// Half a lot is refused before anything is sent.
let half = ud.place(order(ClientOrderId::new_v4(), Qty::new(dec!(0.5)), Price::from(250))).await;
assert!(matches!(half, Err(Error::Rejected { .. })));
// The broker refuses a price (in the mock: 13.37). That is `Rejected`: it did nothing.
let refused = ud.place(order(ClientOrderId::new_v4(), Qty::from(10), refused_price)).await;
assert!(matches!(refused, Err(Error::Rejected { .. })));When the answer is lost, the result is Unknown. Here the mock answers 1.5 s late and the
connector gives up after 200 ms. The order still reaches the exchange, so the right move is to
look, not to place again.
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use brokapi_core::{
ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest,
Price, Qty, ShutdownMode, Side,
};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let cid = ClientOrderId::new_v4();
let req = PlaceOrderRequest {
cid,
instrument: sber,
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(Price::from(250)),
};
assert!(matches!(ud.place(req.clone()).await, Err(Error::Unknown)));
// Do NOT place again. Ask the broker, with a bound on the waiting.
let found = tokio::time::timeout(Duration::from_secs(10), async {
loop {
if let Ok(order) = ud.order_state(cid).await {
break order;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await?;
assert_eq!(found.cid, cid);§fn cancel<'life0, 'async_trait>(
&'life0 self,
cid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn cancel<'life0, 'async_trait>(
&'life0 self,
cid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Cancels an order by the cid you placed it with (CancelOrder by request id).
Ok means the broker accepted the cancel. The final state arrives on the order stream, or ask
order_state. The exchange order id is not needed. Commands on the same
instrument go one at a time. The library does not send a cancel twice on its own.
§Errors
- [
Error::NotReconciled], [Error::Degraded], [Error::ShuttingDown]: the gate is closed, nothing was sent. - [
Error::Rejected] when the broker refuses. The usual reason is codenot_found: the broker has no such order (never placed, or already done). - [
Error::RateLimited] when a quota is used up. - [
Error::Unknown] on a timeout or a broken connection: the order may be cancelled or still working. Checkorder_state.
§Examples
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
let cid = ClientOrderId::new_v4();
ud.place(buy(cid, Price::from(250))).await?;
ud.cancel(cid).await?;
// A cid the broker has never seen is a refusal.
assert!(matches!(ud.cancel(ClientOrderId::new_v4()).await, Err(Error::Rejected { .. })));§fn replace<'life0, 'async_trait>(
&'life0 self,
req: ReplaceOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn replace<'life0, 'async_trait>(
&'life0 self,
req: ReplaceOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
ReplaceOrder needs the exchange order id and the quantity: both come from what the connector
already knows about the original, else from one GetOrderState. req.cid is the new
idempotency key, it replaces the old one.
req.qty of None keeps the original quantity and req.price of None keeps the original
price. A new quantity is a whole number of lots (in units, like in place). The old order is replaced by a new one,
which gets req.cid and a new exchange id. Like place, the call is never repeated: a lost
answer is [Error::Unknown], and req.cid is used up. Look at both orders before you try
again.
§Errors
- [
Error::NotReconciled], [Error::Degraded], [Error::ShuttingDown]: nothing was sent. - [
Error::Rejected] with codeinvalid_qtyorduplicate_place(the newcidwas used before), both before the replace is sent. [Error::Rejected] with codenot_foundwhen the broker does not know the original order. Any other refusal of the broker, with the gRPC status name as code. - [
Error::RateLimited], [Error::Transport] and [Error::Config] while the connector reads the original order (oneGetOrderState, only when it does not know the order). - [
Error::Unknown] on a timeout or a broken connection during the replace itself.
§Examples
use brokapi_core::ReplaceOrderRequest;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
let first = ClientOrderId::new_v4();
ud.place(buy(first, Price::from(250))).await?;
// Same quantity, new price. The new order has a new cid.
let second = ClientOrderId::new_v4();
let ack = ud
.replace(ReplaceOrderRequest { cid: second, original_cid: first, qty: None, price: Some(Price::from(251)) })
.await?;
assert_eq!(ack.cid, second);
// An order the broker has never heard of cannot be replaced.
let ghost = ReplaceOrderRequest { cid: ClientOrderId::new_v4(), original_cid: ClientOrderId::new_v4(), qty: None, price: None };
assert!(matches!(ud.replace(ghost).await, Err(Error::Rejected { .. })));§fn order_state<'life0, 'async_trait>(
&'life0 self,
cid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<OrderSnapshot, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn order_state<'life0, 'async_trait>(
&'life0 self,
cid: Uuid,
) -> Pin<Box<dyn Future<Output = Result<OrderSnapshot, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
The current state of one order by its cid (GetOrderState by request id).
It also works for an order that is already filled or cancelled, so it is the way to learn the
final state. qty and filled are in units of the instrument. The gate is not required: you may call it before
the first reconcile.
§Errors
- [
Error::Rejected] with codenot_foundwhen the broker does not know the order. - [
Error::RateLimited] when a quota is used up. Repeat afterretry_after. - [
Error::Transport] on a timeout or a network failure. A read, so repeat it later. - [
Error::Config] when the broker does not accept the token.
§Examples
use brokapi_core::OrderState;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
let cid = ClientOrderId::new_v4();
ud.place(buy(cid, Price::from(250))).await?;
let snapshot = ud.order_state(cid).await?;
assert_eq!(snapshot.cid, cid);
assert!(matches!(snapshot.state, OrderState::Acked { .. }));
assert!(matches!(ud.order_state(ClientOrderId::new_v4()).await, Err(Error::Rejected { .. })));§fn active_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<OrderSnapshot>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn active_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<OrderSnapshot>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Orders that are live at the broker now (GetOrders) for this account.
Orders that ended (filled, cancelled, rejected) are not listed. An order placed elsewhere
(phone, another program) is listed too, with a stand-in cid made from its exchange id.
qty and filled are in units of the instrument.
§Errors
[Error::RateLimited], [Error::Transport], [Error::Config] and [Error::Rejected], as for
order_state. A read, so repeat it later.
§Examples
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
assert!(ud.active_orders().await?.is_empty());
let cid = ClientOrderId::new_v4();
ud.place(buy(cid, Price::from(250))).await?;
let live = ud.active_orders().await?;
assert_eq!(live.iter().map(|o| o.cid).collect::<Vec<_>>(), vec![cid]);§fn subscribe_order_state<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn subscribe_order_state<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Streams the state changes of your orders (OrderStateStream), Lossless.
The buffer holds order_state_buffer events. Own orders are never dropped: on overflow an
overflow event is sent and the trading gate closes until the next successful
reconcile. Calling it again returns the same subscription id. The stream
reconnects by itself. After a reconnect the connector reads GetOrders and emits what
changed. An order that ended during the gap is not in GetOrders: find it with
order_state.
§Errors
- [
Error::Rejected] when the broker refuses the stream. - [
Error::Transport] when the broker did not confirm the stream withinrequest_timeout.
§Examples
use std::time::Duration;
use brokapi_core::{Event, OrderState};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
let sub = ud.subscribe_order_state().await?;
assert_eq!(ud.subscribe_order_state().await?, sub, "the same id again");
let cid = ClientOrderId::new_v4();
ud.place(buy(cid, Price::from(250))).await?;
let state = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let env = events.recv().await.expect("the channel stays open");
if let Event::Order(snapshot) = env.payload {
if snapshot.cid == cid {
break snapshot.state;
}
}
}
})
.await?;
assert!(matches!(state, OrderState::Acked { .. } | OrderState::InFlight { .. }));§fn reconcile<'life0, 'life1, 'async_trait>(
&'life0 self,
ledger: &'life1 [Uuid],
) -> Pin<Box<dyn Future<Output = Result<ReconcileReport, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
UserDataConnector: 'async_trait,
fn reconcile<'life0, 'life1, 'async_trait>(
&'life0 self,
ledger: &'life1 [Uuid],
) -> Pin<Box<dyn Future<Output = Result<ReconcileReport, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
UserDataConnector: 'async_trait,
One GetOrders call: the live orders at the broker are compared with the ledger. A cid in
the ledger that is not live is closed_offline (filled, cancelled, rejected, or never
accepted - ask order_state for which). Live orders without our cid are foreign.
A successful reconcile opens the command gate and ends Degraded.
ledger is the list of cids you know to be yours (an empty slice on a fresh start). Until
this call succeeds once, every trading command is refused with [Error::NotReconciled].
§Errors
[Error::RateLimited], [Error::Transport], [Error::Config] and [Error::Rejected], as for
active_orders. The gate stays closed when it fails. Repeat the call.
§Examples
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let buy = |cid, price| PlaceOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Buy,
order_type: OrderType::Limit,
qty: Qty::from(10),
price: Some(price),
};
// A run places an order, then the program restarts and knows only the cid from its own record.
ud.reconcile(&[]).await?;
let cid = ClientOrderId::new_v4();
ud.place(buy(cid, Price::from(250))).await?;
let lost = ClientOrderId::new_v4();
let report = ud.reconcile(&[cid, lost]).await?;
assert_eq!(report.confirmed.iter().map(|o| o.cid).collect::<Vec<_>>(), vec![cid]);
// A cid that is not live at the broker: filled, cancelled or never accepted. Ask `order_state`.
assert_eq!(report.closed_offline, vec![lost]);
assert!(report.foreign.is_empty());§impl PortfolioSource for UserDataConnector
impl PortfolioSource for UserDataConnector
§fn portfolio<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Portfolio, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn portfolio<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Portfolio, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Cash from GetPositions, positions from GetPortfolio (two calls).
The two calls are not one atomic snapshot: the account may change between them. Cash lines are
per currency (upper case), with available and blocked. Currency holdings are not repeated
in positions.
§Errors
[Error::RateLimited], [Error::Transport], [Error::Config] and [Error::Rejected], as for
order_state. A read, so repeat it later.
§Examples
use brokapi_core::PortfolioSource;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let portfolio = ud.portfolio().await?;
assert!(!portfolio.balances.is_empty());
assert!(!portfolio.positions.is_empty());§fn positions<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Position>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn positions<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Position>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Quantity in units of the instrument, prices in the settlement currency.
One GetPortfolio call. Currency holdings are left out (see
balances).
§Errors
The same as portfolio.
§Examples
use brokapi_core::PortfolioSource;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let positions = ud.positions().await?;
assert!(positions.iter().all(|p| !p.qty.is_negative()));§fn balances<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Balance>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn balances<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Balance>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Cash by currency from GetPositions: available money and the blocked part.
Currency codes are upper case. One call.
§Errors
The same as portfolio.
§Examples
use brokapi_core::PortfolioSource;
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let cash = ud.balances().await?;
assert!(cash.iter().all(|b| b.available >= Money::from(0)));§fn subscribe_portfolio<'life0, 'async_trait>(
&'life0 self,
opts: SubscribeOpts,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn subscribe_portfolio<'life0, 'async_trait>(
&'life0 self,
opts: SubscribeOpts,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
PortfolioStream (positions with prices) and PositionsStream (cash and balances) under one id.
Two server streams feed one subscription. The events are Event::Portfolio (a full portfolio
from PortfolioStream, with prices, and the latest known cash), Event::Balances (cash
changed) and Event::Positions (positions changed, no prices). Calling it again returns the
same id (the options of the first call stay).
§Errors
The same as subscribe_order_state.
§Examples
use std::time::Duration;
use brokapi_core::{Event, PortfolioSource, SubscribeOpts};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, OrderExecutor, OrderType, PlaceOrderRequest, Price, Qty, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let sub = ud.subscribe_portfolio(SubscribeOpts::conflate()).await?;
let first = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let env = events.recv().await.expect("the channel stays open");
if let Event::Portfolio(p) = env.payload {
break (env.sub, p);
}
}
})
.await?;
assert_eq!(first.0, Some(sub));§impl StopOrderExecutor for UserDataConnector
impl StopOrderExecutor for UserDataConnector
§fn place_stop<'life0, 'async_trait>(
&'life0 self,
req: PlaceStopOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn place_stop<'life0, 'async_trait>(
&'life0 self,
req: PlaceStopOrderRequest,
) -> Pin<Box<dyn Future<Output = Result<OrderAck, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
StopOrdersService/PostStopOrder. cid goes to order_id (the idempotency key), as for
plain orders. The broker’s stop order does not carry it back, so the link cid <-> broker id is
kept in memory: after a restart active_stop_orders shows cid: None and a stop order is
cancelled by StopOrderRef::Broker.
After [Error::Unknown] the link is made later. The connector keeps what was asked for
(instrument, side, kind, lots, stop price, limit price) and the time the request was in flight.
The next active_stop_orders that finds exactly one stop order with
no cid of ours that fits this request, and was created within that time (give or take 10 seconds for
the difference of the clocks), shows it with this cid, and from then on the stop order can be
cancelled by it. If two stop orders fit, or two lost placements are equal, nothing is linked
and the stop order shows cid: None: the connector does not guess. Read the list after the
answer was lost, and do not place again under another cid while a stop order with cid: None of the
same instrument and side is there. This rests on the creation time that T-Bank puts in the list.
Sending the same request again with the same order_id is not used: whether T-Bank treats
order_id of a stop order as an idempotency key is not verified live.
Units: qty is a whole number of LOTS (at least 1). Prices are positive. A stop-limit order
needs a limit price. Expiry is GoodTillCancel or GoodTillDate: T-Bank stop orders have no
end-of-day expiry. The checks run before any quota is spent. Like place, the call is never
repeated: a lost answer is [Error::Unknown], and so is an answer with no stop order id
(the broker may hold the order, nobody can tell). In both cases the cid is used up.
§Errors
- [
Error::NotReconciled], [Error::Degraded], [Error::ShuttingDown]: nothing was sent. - [
Error::Rejected] with codeinvalid_qty,invalid_price,missing_limit_priceorunsupported_expiryfor a bad request, orduplicate_placefor acidused before. Nothing was sent. A refusal by the broker is [Error::Rejected] with the gRPC status name as code. After a refusal by the broker thecidmay be used again. - [
Error::RateLimited] when a quota is used up. Nothing was sent. - [
Error::Unknown] on a timeout, a broken connection, a server fault or an answer without an id. Checkactive_stop_ordersbefore you place again.
§Examples
use brokapi_core::{
OrderExecutor, PlaceStopOrderRequest, Price, Qty, StopExpiry, StopOrderExecutor, StopOrderKind, StopOrderRef,
};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
let cid = ClientOrderId::new_v4();
let stop_loss = PlaceStopOrderRequest {
cid,
instrument: sber.clone(),
side: Side::Sell,
kind: StopOrderKind::StopLoss,
qty: Qty::from(10),
stop_price: Price::from(200),
limit_price: None, // market order at the trigger
expiry: StopExpiry::GoodTillCancel,
};
let ack = ud.place_stop(stop_loss).await?;
assert_eq!(ud.active_stop_orders().await?[0].cid, Some(cid));
// A stop-limit order without a limit price is refused before anything is sent.
let bad = PlaceStopOrderRequest {
cid: ClientOrderId::new_v4(),
instrument: sber,
side: Side::Sell,
kind: StopOrderKind::StopLimit,
qty: Qty::from(10),
stop_price: Price::from(200),
limit_price: None,
expiry: StopExpiry::GoodTillCancel,
};
assert!(matches!(ud.place_stop(bad).await, Err(Error::Rejected { .. })));
ud.cancel_stop(StopOrderRef::Broker(ack.broker_id)).await?;
assert!(ud.active_stop_orders().await?.is_empty());§fn cancel_stop<'life0, 'async_trait>(
&'life0 self,
stop: StopOrderRef,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn cancel_stop<'life0, 'async_trait>(
&'life0 self,
stop: StopOrderRef,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
Cancels a stop order by its cid or by the broker’s id (CancelStopOrder).
A cid works only for a stop order placed by this connector object: the link lives in
memory and is lost on restart. After a restart cancel by StopOrderRef::Broker, taking the
id from active_stop_orders. Not repeated automatically.
§Errors
- [
Error::NotReconciled], [Error::Degraded], [Error::ShuttingDown]: nothing was sent. - [
Error::Rejected] with codeunknown_stop_orderwhen thecidis not known to this connector. The broker’sNotFoundisnot_found, other refusals come with the gRPC status name as code. - [
Error::RateLimited] when a quota is used up. - [
Error::Unknown] on a timeout or a broken connection: the stop order may or may not be cancelled. Checkactive_stop_orders.
§Examples
use brokapi_core::{OrderExecutor, StopOrderExecutor, StopOrderRef};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
// A cid this connector never placed cannot be mapped to a broker id.
let result = ud.cancel_stop(StopOrderRef::Cid(ClientOrderId::new_v4())).await;
match result {
Err(Error::Rejected { code, .. }) => assert_eq!(code, "unknown_stop_order"),
other => panic!("expected unknown_stop_order, got {other:?}"),
}§fn active_stop_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<StopOrderSnapshot>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn active_stop_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<StopOrderSnapshot>, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
The stop orders that are active now (GetStopOrders with the active filter).
cid is filled only for stop orders placed by this connector object. Others (placed earlier,
by hand, by another program) have cid: None: use their broker_id. A stop order whose
placement ended in [Error::Unknown] gets its cid here, when it can be told for sure, see
place_stop.
§Errors
[Error::RateLimited], [Error::Transport], [Error::Config] and [Error::Rejected], as for
order_state. A read, so repeat it later.
The broker refuses to answer with too many stop orders at once (error 30261, “narrow the request with
from and to”). The connector first asks for the whole list. On 30261 it halves the period until every
piece fits and merges the pieces, up to 40 requests. If that is still not enough, the error is
[Error::Rejected] with the code stop_orders_response_limit. Which time of a stop order from and to
compare with, and how many stop orders one answer may hold, are not documented and not verified live.
§Examples
use brokapi_core::{OrderExecutor, StopOrderExecutor};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
assert!(ud.active_stop_orders().await?.is_empty());§fn subscribe_stop_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
fn subscribe_stop_orders<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionId, Error>> + Send + 'async_trait>>where
'life0: 'async_trait,
UserDataConnector: 'async_trait,
T-Bank has no stream of stop order statuses (StopOrdersService is unary only).
§Errors
Always [Error::Unsupported] (stop_order_stream), without a network call. The library does
not fake the stream by polling. Poll active_stop_orders yourself.
§Examples
use brokapi_core::{OrderExecutor, StopOrderExecutor};
use brokapi_core::{ClientOrderId, Error, InstrumentId, InstrumentKey, NativeId, ShutdownMode, Side};
use brokapi_runtime::BrokerLimiter;
use brokapi_tbank::limits::default_limits;
use brokapi_tbank::{UserDataConfig, UserDataConnector};
use tokio::sync::mpsc;
let (events_tx, mut _events) = mpsc::channel(256);
let ud = UserDataConnector::start(UserDataConfig::new(cfg, "tbank-ud"), BrokerLimiter::new(&default_limits()), events_tx).await?;
ud.reconcile(&[]).await?;
let sber = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
assert!(matches!(ud.subscribe_stop_orders().await, Err(Error::Unsupported { .. })));Auto Trait Implementations§
impl !RefUnwindSafe for UserDataConnector
impl !UnwindSafe for UserDataConnector
impl Freeze for UserDataConnector
impl Send for UserDataConnector
impl Sync for UserDataConnector
impl Unpin for UserDataConnector
impl UnsafeUnpin for UserDataConnector
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request§impl<L> LayerExt<L> for L
impl<L> LayerExt<L> for L
§fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>where
L: Layer<S>,
fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>where
L: Layer<S>,
Layered].