pub struct BcsHttpClient { /* private fields */ }Expand description
The authorized HTTP client of the BCS Trade API.
It holds the refresh token and the current access token, adds Authorization: Bearer ... to every
call, and handles the token life cycle for you:
- The first call trades the refresh token for an access token. The access token is cached.
- The client tracks the expiry time: a token past 90% of its
expires_inis renewed before the next call. - When the broker answers 401 to a read, the client gets a new access token and repeats the same request once. If the second answer is 401 again, you get the error of that answer.
- A command (placing, changing or cancelling an order) is never repeated, not even after a 401:
the client renews the token for the next call and returns the 401 as an error, see
BcsHttpClient::post_command. - A transport error or a timeout is never repeated. This matters for commands: see
BcsApiError::HttpError. - If the token service returns a new refresh token (rotation), the client keeps it in memory. See
BcsHttpClient::current_refresh_token.
The client is cheap to clone. Clones share the tokens and the connection pool. It can be used from many tasks at once.
The endpoint methods (one per REST call) are documented on this type too, they are in
super::endpoints. The broker limits calls per account (about 10 per second for most services,
3 per second for non-trade operations, per the broker docs). The client does not throttle, see
crate::utils::rate_limiter::RateLimiter.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::OrderBookRequest;
let client = BcsHttpClient::new(cfg)?;
let book = client
.get_order_book(&OrderBookRequest { ticker: "SBER".into(), class_code: "TQBR".into(), depth: 5 })
.await?;
println!("{} levels of bids", book.bids.len());Implementations§
Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub fn new(config: BcsConfig) -> Result<Self, BcsApiError>
pub fn new(config: BcsConfig) -> Result<Self, BcsApiError>
Creates a client from a configuration. No network call is made.
The HTTP connection pool is created with the timeout of BcsConfig::request_timeout and
gzip enabled. The client_id of later token requests is trade-api-write when
BcsConfig::is_write is set, otherwise trade-api-read.
§Errors
BcsApiError::HttpError (no status) if the HTTP stack cannot be built, for example when the
TLS backend fails to initialise.
§Examples
use brokapi_bcs_protocol::config::BcsConfig;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(BcsConfig::new("refresh-token".to_string()))?;
assert_eq!(client.base_url(), "https://be.broker.ru");Sourcepub async fn get_access_token(&self) -> Result<String, BcsApiError>
pub async fn get_access_token(&self) -> Result<String, BcsApiError>
Returns the cached access token, or gets a new one if there is none yet.
A token that is past 90% of its expires_in is renewed here, before it is used, so a command does
not meet a 401 after a day of work. A 401 on a read is still repaired by one refresh and one
repeat. The WebSocket session calls this at every (re)connect, and forces a refresh itself when
the broker refuses the connection.
§Errors
As BcsHttpClient::refresh_access_token, and only when there is no cached token or it is
(nearly) expired.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let first = client.get_access_token().await?;
let second = client.get_access_token().await?; // from the cache, no second token request
assert_eq!(first, second);Sourcepub async fn refresh_access_token(&self) -> Result<String, BcsApiError>
pub async fn refresh_access_token(&self) -> Result<String, BcsApiError>
Trades the refresh token for a new access token and stores it. Always calls the token service.
Sends grant_type=refresh_token, the refresh token and the client_id as a form to
AUTH_TOKEN_PATH. On success the new access token is cached. If the reply carries a new
refresh token it replaces the old one in memory.
Refreshes are serialised by a lock, so two tasks never talk to the token service at the same moment. The lock does not merge them: each caller still makes its own request, one after the other.
§Errors
BcsApiError::AuthError: the token service refused the refresh token.messageis itserror_description(orerror).BcsApiError::HttpError: a transport failure, an unreadable success body, or an error status whose body is not the token service’s JSON error.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let token = client.refresh_access_token().await?;
assert!(!token.is_empty());Sourcepub async fn get<T: DeserializeOwned>(
&self,
path: &str,
) -> Result<T, BcsApiError>
pub async fn get<T: DeserializeOwned>( &self, path: &str, ) -> Result<T, BcsApiError>
GETs a path and parses the JSON reply into T.
path starts with / and is added to the base URL, for example
/trade-api-bff-portfolio/api/v1/portfolio. Use it for an endpoint that has no typed method.
The typed methods of super::endpoints call it.
A 401 is answered with one token refresh and one repeat. Other failures are not retried.
§Errors
BcsApiError::ValidationErrorandBcsApiError::NotFound: the broker saidVALIDATION_ERRORorNOT_FOUNDin its JSON error body.BcsApiError::RateLimitExceeded: HTTP 429 without the JSON error body.BcsApiError::HttpError: any other non-success status, or a transport failure.BcsApiError::ParseError: the reply is a success but is not the JSON thatTexpects.BcsApiError::AuthError: the token refresh after a 401 failed.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let portfolio: serde_json::Value = client.get("/trade-api-bff-portfolio/api/v1/portfolio").await?;
assert!(portfolio.is_array());
// A path the broker does not know ends in an HttpError that carries the status.
let err = client.get::<serde_json::Value>("/no/such/path").await.unwrap_err();
assert!(matches!(err, BcsApiError::HttpError { status: Some(404), .. }));Sourcepub async fn get_with_query<T, Q>(
&self,
path: &str,
query: &Q,
) -> Result<T, BcsApiError>where
T: DeserializeOwned,
Q: Serialize,
pub async fn get_with_query<T, Q>(
&self,
path: &str,
query: &Q,
) -> Result<T, BcsApiError>where
T: DeserializeOwned,
Q: Serialize,
GETs a path with query parameters and parses the JSON reply into T.
query is anything serde can turn into a URL query: a struct, a map, or a slice of pairs.
Retry and errors are the same as for BcsHttpClient::get.
§Errors
The same as BcsHttpClient::get.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let book: serde_json::Value = client
.get_with_query(
"/trade-api-market-data-connector/api/v1/order-book",
&[("ticker", "SBER"), ("classCode", "TQBR"), ("depth", "5")],
)
.await?;
assert_eq!(book["ticker"], "SBER");Sourcepub async fn get_raw(
&self,
path: &str,
) -> Result<(StatusCode, String), BcsApiError>
pub async fn get_raw( &self, path: &str, ) -> Result<(StatusCode, String), BcsApiError>
GETs a path and returns the status and the body text, without parsing and without mapping
errors.
An error status is not an Err here: you get it as the first element of the pair. Use it
to look at what the broker really sent. Only a 401 is handled (one refresh, one repeat).
§Errors
BcsApiError::HttpError (no status) on a transport failure, and the errors of
BcsHttpClient::refresh_access_token when a 401 needs a new token.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let (status, body) = client
.get_raw("/trade-api-market-data-connector/api/v1/order-book?ticker=SBER&classCode=TQBR")
.await?;
assert!(status.is_success());
assert!(body.contains("bids"));Sourcepub async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, BcsApiError>where
T: DeserializeOwned,
B: Serialize,
pub async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, BcsApiError>where
T: DeserializeOwned,
B: Serialize,
POSTs a JSON body to a path and parses the JSON reply into T.
A 401 is answered with one token refresh and one repeat of the same body. Use this for
requests that only read (a POST that searches or asks for quotes). For a command use
BcsHttpClient::post_command, which never repeats. A transport error or a timeout is returned
as it is and is never repeated.
§Errors
The same as BcsHttpClient::get.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let body = serde_json::json!({ "instruments": [{ "ticker": "SBER", "classCode": "TQBR" }] });
let quotes: serde_json::Value = client
.post("/trade-api-market-data-connector/api/v1/quotes", &body)
.await?;
assert!(quotes["records"].is_array());Sourcepub async fn post_command<T, B>(
&self,
path: &str,
body: &B,
) -> Result<T, BcsApiError>where
T: DeserializeOwned,
B: Serialize,
pub async fn post_command<T, B>(
&self,
path: &str,
body: &B,
) -> Result<T, BcsApiError>where
T: DeserializeOwned,
B: Serialize,
POSTs a command (place, change or cancel an order) and parses the JSON reply into T. Never
repeats the request.
A repeat of a command is the one way to place an order twice, so the client sends it once. If the
broker answers 401, the client renews the access token (so the next call works) and returns the
401 as BcsApiError::HttpError with status: Some(401). A 401 means the broker did not act, so
it is safe to send the command again with the same client order id, if you want to. A transport
error or a timeout is an unknown outcome, as for every command.
§Errors
The same as BcsHttpClient::post.
Sourcepub fn inner_client(&self) -> &Client
pub fn inner_client(&self) -> &Client
The underlying reqwest client, for calls this crate does not wrap.
It has the timeout and gzip setting of the configuration but carries no authorization.
Add the token yourself with BcsHttpClient::get_access_token.
§Examples
use brokapi_bcs_protocol::config::BcsConfig;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(BcsConfig::new("t".to_string()))?;
let _http: &reqwest::Client = client.inner_client();Sourcepub async fn current_refresh_token(&self) -> String
pub async fn current_refresh_token(&self) -> String
The refresh token the client holds now.
It is the one from the configuration until the token service returns a rotated one, then it is the new one. Persist it yourself if you want a restart to use the rotated token (the configuration is not updated). Treat it as a secret.
§Examples
use brokapi_bcs_protocol::config::BcsConfig;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(BcsConfig::new("my-refresh-token".to_string()))?;
assert_eq!(client.current_refresh_token().await, "my-refresh-token");Sourcepub fn base_url(&self) -> &str
pub fn base_url(&self) -> &str
The REST base URL this client calls, without a trailing slash.
§Examples
use brokapi_bcs_protocol::config::BcsConfig;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(BcsConfig::for_mock("t", "http://127.0.0.1:1"))?;
assert_eq!(client.base_url(), "http://127.0.0.1:1");Sourcepub fn ws_base_url(&self) -> &str
pub fn ws_base_url(&self) -> &str
The WebSocket base URL that sessions started from this client connect to.
§Examples
use brokapi_bcs_protocol::config::BcsConfig;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(BcsConfig::for_mock("t", "http://127.0.0.1:1"))?;
assert_eq!(client.ws_base_url(), "ws://127.0.0.1:1");Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn authenticate(&self) -> Result<TokenResponse, BcsApiError>
pub async fn authenticate(&self) -> Result<TokenResponse, BcsApiError>
Gets a fresh access token and reports it in a TokenResponse.
It calls BcsHttpClient::refresh_access_token, so the token service is contacted every time.
The returned value is built locally, not copied from the broker’s reply: expires_in and
refresh_expires_in are always 0, token_type is the fixed text Bearer, session_state
and scope are None, and refresh_token is the refresh token the client holds now. Read the
lifetimes from the broker docs (24 hours and 90 days), not from this value.
You do not have to call it: every other method authorizes itself.
§Errors
As BcsHttpClient::refresh_access_token: BcsApiError::AuthError if the refresh token is
refused, BcsApiError::HttpError on transport failures.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let token = client.authenticate().await?;
assert!(!token.access_token.is_empty());
assert_eq!(token.token_type, "Bearer");Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn get_daily_schedule(
&self,
req: &DailyScheduleRequest,
) -> Result<DailyScheduleResponse, BcsApiError>
pub async fn get_daily_schedule( &self, req: &DailyScheduleRequest, ) -> Result<DailyScheduleResponse, BcsApiError>
Gets the trading schedule of one instrument for the day: GET .../trading-schedule/daily-schedule.
Both class_code and ticker are required by the broker. The reply lists the sessions of the
day with their start and end as times of day (HH:MM:SS). The broker docs do not say in which
time zone those times are, and the names of the sessions are in Russian (for example
“Торговый период”). Not verified against the live API.
§Errors
BcsApiError::ValidationError if a parameter is missing or empty. See BcsApiError for the
other variants.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::DailyScheduleRequest;
let client = BcsHttpClient::new(cfg)?;
let day = client
.get_daily_schedule(&DailyScheduleRequest { class_code: "TQBR".into(), ticker: "SBER".into() })
.await?;
assert!(day.is_work_day);
assert!(!day.daily_schedule.is_empty());Sourcepub async fn get_trading_status(
&self,
req: &TradingStatusRequest,
) -> Result<TradingStatusResponse, BcsApiError>
pub async fn get_trading_status( &self, req: &TradingStatusRequest, ) -> Result<TradingStatusResponse, BcsApiError>
Gets the current trading session status of a board: GET .../trading-schedule/status.
The status is per class code (board), not per instrument. The reply says whether the session is open and when the status changes next.
§Errors
BcsApiError::ValidationError if class_code is missing. BcsApiError::ParseError if the
reply carries a nextSessionDate that is not an ISO 8601 date-time. See BcsApiError for the
other variants.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::TradingStatusRequest;
let client = BcsHttpClient::new(cfg)?;
let status = client
.get_trading_status(&TradingStatusRequest { class_code: "TQBR".into() })
.await?;
assert_eq!(status.trading_session_status, "OPEN");Sourcepub async fn get_instruments_by_isins(
&self,
req: &InstrumentsByIsinsRequest,
size: Option<u32>,
page: Option<u32>,
) -> Result<Vec<InstrumentInfo>, BcsApiError>
pub async fn get_instruments_by_isins( &self, req: &InstrumentsByIsinsRequest, size: Option<u32>, page: Option<u32>, ) -> Result<Vec<InstrumentInfo>, BcsApiError>
Looks up instruments by ISIN: POST .../instruments/by-isins.
size (1 to 100, default 50) and page (from 0) page the answer. The broker does not say how
many pages there are: a page shorter than size is the last one, a full page means “ask for
page + 1”. The parameters are sent in the query string (the broker docs mark them as path
parameters, but the path has none, not verified against the live API).
§Errors
See BcsApiError. An unknown ISIN is probably just missing from the list (not verified
against the live API).
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::InstrumentsByIsinsRequest;
let client = BcsHttpClient::new(cfg)?;
let found = client
.get_instruments_by_isins(
&InstrumentsByIsinsRequest { isins: vec!["RU0009029540".into()] },
Some(50),
Some(0),
)
.await?;
assert_eq!(found[0].ticker, "SBER");Sourcepub async fn get_instruments_by_tickers(
&self,
req: &InstrumentsByTickersRequest,
size: Option<u32>,
page: Option<u32>,
) -> Result<Vec<InstrumentInfo>, BcsApiError>
pub async fn get_instruments_by_tickers( &self, req: &InstrumentsByTickersRequest, size: Option<u32>, page: Option<u32>, ) -> Result<Vec<InstrumentInfo>, BcsApiError>
Looks up instruments by ticker: POST .../instruments/by-tickers.
One ticker can trade on several boards, so the answer has an InstrumentInfo per instrument
and each lists its InstrumentInfo::boards. Paging works as in
BcsHttpClient::get_instruments_by_isins.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::InstrumentsByTickersRequest;
let client = BcsHttpClient::new(cfg)?;
let found = client
.get_instruments_by_tickers(&InstrumentsByTickersRequest { tickers: vec!["SBER".into()] }, None, None)
.await?;
let params = found[0].trading_params_primary().expect("a primary board");
assert_eq!(params.class_code, "TQBR");Sourcepub async fn get_instruments_by_type(
&self,
req: &InstrumentsByTypeRequest,
) -> Result<Vec<InstrumentInfo>, BcsApiError>
pub async fn get_instruments_by_type( &self, req: &InstrumentsByTypeRequest, ) -> Result<Vec<InstrumentInfo>, BcsApiError>
Lists instruments of one kind: GET .../instruments/by-type.
InstrumentsByTypeRequest::instrument_type must be one of the broker’s names: CURRENCY,
STOCK, FOREIGN_STOCK, BONDS, NOTES, DEPOSITARY_RECEIPTS, EURO_BONDS, MUTUAL_FUNDS,
ETF, FUTURES, OPTIONS, GOODS, INDICES. For OPTIONS the broker also requires the base
asset ticker. A full list is long, so read it page by page: size 1 to 100, page from 0, a
page shorter than size is the last.
§Errors
BcsApiError::ValidationError for an unknown type, a missing base asset for options, or a
size outside 1 to 100. See BcsApiError for the other variants.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::InstrumentsByTypeRequest;
let client = BcsHttpClient::new(cfg)?;
let mut req = InstrumentsByTypeRequest {
instrument_type: "STOCK".into(),
base_asset_ticker: None,
size: Some(100),
page: Some(0),
};
let stocks = client.get_instruments_by_type(&req).await?;
assert!(!stocks.is_empty());
// A name the broker does not know is refused.
req.instrument_type = "Stock".into();
let err = client.get_instruments_by_type(&req).await.unwrap_err();
assert!(matches!(err, BcsApiError::ValidationError { .. }));Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn get_limits(&self) -> Result<LimitsResponse, BcsApiError>
pub async fn get_limits(&self) -> Result<LimitsResponse, BcsApiError>
Gets the account limits: positions, money and futures limits (GET .../limits).
It is a snapshot of what the account holds and what is locked by active orders. The same data
comes as a stream on the limits WebSocket channel, see crate::ws::client::BcsWsClient::connect_limits.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let limits = client.get_limits().await?;
let rub = limits.money_limits.iter().find(|m| m.currency.as_deref() == Some("RUB"));
assert!(rub.is_some());Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn get_instruments_discounts(
&self,
) -> Result<InstrumentDiscountsResponse, BcsApiError>
pub async fn get_instruments_discounts( &self, ) -> Result<InstrumentDiscountsResponse, BcsApiError>
Gets the margin discounts of instruments (GET .../instruments-discounts).
A discount is the broker’s rate for an instrument bought on margin (long) or sold short. The broker docs give the field names but not the unit (fraction or percent), not verified against the live API.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let discounts = client.get_instruments_discounts().await?;
assert_eq!(discounts[0].ticker, "SBER");Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn get_candles(
&self,
request: &CandlesRequest,
) -> Result<CandlesResponse, BcsApiError>
pub async fn get_candles( &self, request: &CandlesRequest, ) -> Result<CandlesResponse, BcsApiError>
Gets historical candles: GET .../candles-chart.
The broker allows at most 1440 bars in one request. A longer interval is refused, so split
it or use a larger CandlesRequest::time_frame. The time frame names are
M1 M5 M15 M30 H1 H4 D W MN. Times are UTC.
§Errors
BcsApiError::ValidationError for a time frame the broker does not know or an interval of
more than 1440 bars. See BcsApiError for the other variants.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::CandlesRequest;
use chrono::{TimeZone, Utc};
let client = BcsHttpClient::new(cfg)?;
let mut req = CandlesRequest {
ticker: "SBER".into(),
class_code: "TQBR".into(),
start_date: Utc.with_ymd_and_hms(2026, 10, 19, 7, 0, 0).unwrap(),
end_date: Utc.with_ymd_and_hms(2026, 10, 19, 8, 0, 0).unwrap(),
time_frame: "M1".into(),
};
let candles = client.get_candles(&req).await?;
assert!(!candles.bars.is_empty());
// "M10" is not a time frame of the broker.
req.time_frame = "M10".into();
let err = client.get_candles(&req).await.unwrap_err();
assert!(matches!(err, BcsApiError::ValidationError { .. }));Sourcepub async fn get_order_book(
&self,
request: &OrderBookRequest,
) -> Result<OrderBookResponse, BcsApiError>
pub async fn get_order_book( &self, request: &OrderBookRequest, ) -> Result<OrderBookResponse, BcsApiError>
Gets a snapshot of the order book: GET .../order-book.
depth is 1 to 20 levels per side (the broker default is 20). For a stream of changes use the
market data WebSocket, see crate::ws::client::BcsWsClient::subscribe_order_book.
§Errors
BcsApiError::RateLimitExceeded on a bare HTTP 429. BcsApiError::ValidationError for a
depth outside the range or an unknown instrument. See BcsApiError for the other variants.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::OrderBookRequest;
let client = BcsHttpClient::new(cfg)?;
let req = OrderBookRequest { ticker: "SBER".into(), class_code: "TQBR".into(), depth: 10 };
let book = client.get_order_book(&req).await?;
assert!(!book.bids.is_empty() && !book.asks.is_empty());
// When the broker says "too many requests" you get RateLimitExceeded: wait, then call again.
let err = client.get_order_book(&req).await.unwrap_err();
assert!(matches!(err, BcsApiError::RateLimitExceeded));Sourcepub async fn get_last_trades(
&self,
request: &LastTradesRequest,
) -> Result<LastTradesResponse, BcsApiError>
pub async fn get_last_trades( &self, request: &LastTradesRequest, ) -> Result<LastTradesResponse, BcsApiError>
Gets the anonymous trades of the current session: POST .../last-trades.
The request names the instrument and may narrow the period and the side. The broker has no
limit parameter. The reply is read in both shapes seen so far, {"records": [...]} (the
docs) and a bare array (older samples). Records carry no instrument, it is the one you asked
for.
§Errors
BcsApiError::ValidationError if ticker or class_code is missing. See BcsApiError for
the other variants.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::LastTradesRequest;
let client = BcsHttpClient::new(cfg)?;
let trades = client
.get_last_trades(&LastTradesRequest {
ticker: "SBER".into(),
class_code: "TQBR".into(),
..Default::default()
})
.await?;
assert!(trades.iter().all(|t| t.price > dec!(0.0)));Sourcepub async fn get_quotes(
&self,
request: &QuotesRequest,
) -> Result<QuotesResponse, BcsApiError>
pub async fn get_quotes( &self, request: &QuotesRequest, ) -> Result<QuotesResponse, BcsApiError>
Gets current quotes for a list of instruments (1 to 100): POST .../quotes.
Every field of a QuoteRecord except the key may be missing, so check before you use it.
§Errors
BcsApiError::ValidationError for an empty list or more than 100 instruments (the docs’
range, not verified against the live API). See BcsApiError for the other variants.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{QuoteInstrument, QuotesRequest};
let client = BcsHttpClient::new(cfg)?;
let quotes = client
.get_quotes(&QuotesRequest { instruments: vec![QuoteInstrument::new("SBER", "TQBR")] })
.await?;
let sber = "es.records[0];
assert!(sber.bid.is_some() && sber.offer.is_some());Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn search_orders(
&self,
request: OrderSearchRequest,
page: Option<i32>,
size: Option<i32>,
) -> Result<OrderSearchResponse, BcsApiError>
pub async fn search_orders( &self, request: OrderSearchRequest, page: Option<i32>, size: Option<i32>, ) -> Result<OrderSearchResponse, BcsApiError>
Lists your orders by filter: POST .../orders/search.
The list holds only orders placed since 2026-01-26 and at most the 1000 latest (broker docs).
size is 1 to 100 (default 50), page starts at 0. The records carry the broker’s own order id
and not your clientOrderId. To find an order by your id use BcsHttpClient::get_order.
The status codes of this list differ from the codes of order events: see OrderRecord.
§Errors
BcsApiError::ValidationError for a bad period or filter. See BcsApiError for the other
variants.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CreateOrderRequest, OrderSearchRequest};
use brokapi_bcs_protocol::models::enums::OrderSide;
use chrono::{Duration, Utc};
let client = BcsHttpClient::new(cfg)?;
let now = Utc::now();
let request = OrderSearchRequest {
start_date_time: now - Duration::hours(24),
end_date_time: now,
side: None,
order_status: Some(vec![3]), // 3 = active in the order list
order_types: None,
tickers: Some(vec!["SBER".into()]),
class_codes: None,
};
let page = client.search_orders(request, Some(0), Some(50)).await?;
assert_eq!(page.records.len(), 1);Sourcepub async fn search_orders_simple(
&self,
tickers: Option<Vec<String>>,
hours: i64,
) -> Result<OrderSearchResponse, BcsApiError>
pub async fn search_orders_simple( &self, tickers: Option<Vec<String>>, hours: i64, ) -> Result<OrderSearchResponse, BcsApiError>
Lists your orders of the last hours hours, optionally for some tickers.
A shortcut over BcsHttpClient::search_orders: the period is now - hours to now by the
local clock, there is no other filter, and the first page of the default size (50) is returned.
It does not read further pages: when total_pages of the answer is above 1, call
BcsHttpClient::search_orders yourself.
§Errors
The same as BcsHttpClient::search_orders.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let recent = client.search_orders_simple(Some(vec!["SBER".to_string()]), 24).await?;
assert_eq!(recent.total_records, 0); // a fresh mock account has no ordersSourcepub async fn create_order(
&self,
request: CreateOrderRequest,
) -> Result<OrderOperationResponse, BcsApiError>
pub async fn create_order( &self, request: CreateOrderRequest, ) -> Result<OrderOperationResponse, BcsApiError>
Places an order and returns when the broker has accepted the request: POST .../orders.
Needs a write token (crate::config::BcsConfig::is_write). The reply (status) only
confirms that the request was taken. It does not say that the order is on the exchange or
filled. The state comes later on the order events channel
(crate::ws::client::BcsWsClient::connect_order_events) or from BcsHttpClient::get_order.
To get the state in the reply use BcsHttpClient::create_order_sync.
Your client_order_id is the id you must keep. The broker docs say it is used to track the
order and to prevent duplicates. The quantity is in pieces, not in lots.
§No automatic retry
If the call ends with a timeout or a transport error, do not send it again. The order may
exist. A repeat could place a second order. Look the order up first with
BcsHttpClient::get_order by the same client_order_id: an answer means it exists,
BcsApiError::NotFound means it does not (not verified against the live API how long the
broker needs before the order becomes visible).
§Errors
BcsApiError::ValidationError: the broker refused the order, nothing was placed.BcsApiError::HttpErrorwithFORBIDDENorUSER_BLOCKEDinmessage: no trading rights, for example a read token (not verified against the live API which type the broker uses).BcsApiError::RateLimitExceededorBcsApiError::HttpErrorwith status 429: too many requests. The broker also caps orders per day (200000 by its docs).BcsApiError::HttpErrorwithout a status orBcsApiError::ParseError: unknown outcome, see above.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::CreateOrderRequest;
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
// Buy 1 piece of SBER at 300.00. Keep `cid`: it is how you find this order later.
let cid = Uuid::new_v4();
let ack = client
.create_order(CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
assert_eq!(ack.client_order_id, cid);
// A refusal is a ValidationError and means "no order". (The imitation refuses this price.)
let refused = client
.create_order(CreateOrderRequest::limit(
Uuid::new_v4(), "SBER", "TQBR", OrderSide::Buy, 1, brokapi_conformance::REJECT_PRICE.decimal(),
))
.await;
assert!(matches!(refused, Err(BcsApiError::ValidationError { .. })));Sourcepub async fn cancel_order(
&self,
original_client_order_id: Uuid,
request: CancelOrderRequest,
) -> Result<OrderOperationResponse, BcsApiError>
pub async fn cancel_order( &self, original_client_order_id: Uuid, request: CancelOrderRequest, ) -> Result<OrderOperationResponse, BcsApiError>
Cancels an order, the deprecated form with the id in the path: POST .../orders/{id}/cancel.
The broker docs list this method in their deprecated section. Use
BcsHttpClient::cancel_order_by_id. The in-process imitation does not serve the deprecated
forms, so this method has no runnable example. original_client_order_id is the
client_order_id of the order to cancel, request.client_order_id is the id of this request.
§Errors
See BcsApiError. A cancel that timed out has an unknown outcome: ask for the order with
BcsHttpClient::get_order before you decide.
Sourcepub async fn modify_order(
&self,
original_client_order_id: Uuid,
request: ModifyOrderRequest,
) -> Result<OrderOperationResponse, BcsApiError>
pub async fn modify_order( &self, original_client_order_id: Uuid, request: ModifyOrderRequest, ) -> Result<OrderOperationResponse, BcsApiError>
Changes an order, the deprecated form with the id in the path: POST .../orders/{id}.
The broker docs list this method in their deprecated section. Use BcsHttpClient::edit_order.
The in-process imitation does not serve the deprecated forms, so this method has no runnable
example.
§Errors
See BcsApiError.
Sourcepub async fn create_order_sync(
&self,
request: &CreateOrderRequest,
) -> Result<OrderStateListResponse, BcsApiError>
pub async fn create_order_sync( &self, request: &CreateOrderRequest, ) -> Result<OrderStateListResponse, BcsApiError>
Places an order and waits for the broker to process it: POST .../orders/sync.
The body is the same as for BcsHttpClient::create_order. The reply is the current state of
the order, in the format of BcsHttpClient::get_order. A refusal of the exchange can show up
as an order in status “rejected” (orderStatus = 8) with a rejectReason and a normal HTTP 200,
and not as an error (not verified against the live API). Check
OrderExecutionData::order_status and OrderExecutionData::reject_reason, not only Ok.
The rules for timeouts are the same as for BcsHttpClient::create_order: never repeat a
placement that ended without an answer. Look the order up first.
§Errors
The same as BcsHttpClient::create_order.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::CreateOrderRequest;
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
let state = client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
let report = &state.data[0];
assert_eq!(report.order_status.as_deref(), Some("0")); // 0 = new
assert_eq!(report.ticker.as_deref(), Some("SBER"));Sourcepub async fn cancel_order_by_id(
&self,
request: &CancelByIdRequest,
) -> Result<OrderOperationResponse, BcsApiError>
pub async fn cancel_order_by_id( &self, request: &CancelByIdRequest, ) -> Result<OrderOperationResponse, BcsApiError>
Cancels an order named in the body: POST .../orders/cancel.
This is the current form (the id is in the body, not in the path). The reply only confirms that
the cancel request was taken. The order is cancelled when its state says so: watch the order
events channel or call BcsHttpClient::get_order, or use BcsHttpClient::cancel_order_by_id_sync.
The broker allows to cancel orders in status new (0), partly filled (1), replaced (5) and
waiting for confirmation (10).
A cancel that ended without an answer has an unknown outcome. Asking again is harmless in principle (the order is cancelled once), but check the order first.
§Errors
BcsApiError::NotFound: no such order (not verified against the live API).BcsApiError::ValidationError: the order is not in a state that can be cancelled.- Other variants as in
BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CancelByIdRequest, CreateOrderRequest};
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
// `cid` names the order, the second id names this cancel request.
let ack = client
.cancel_order_by_id(&CancelByIdRequest::by_client_order_id(cid, Uuid::new_v4()))
.await?;
assert_eq!(ack.status, "OK");Sourcepub async fn cancel_order_by_id_sync(
&self,
request: &CancelByIdRequest,
) -> Result<OrderStateListResponse, BcsApiError>
pub async fn cancel_order_by_id_sync( &self, request: &CancelByIdRequest, ) -> Result<OrderStateListResponse, BcsApiError>
Cancels an order and waits for the result: POST .../orders/cancel/sync.
The reply is the order state after the cancel, in the format of BcsHttpClient::get_order:
look at OrderExecutionData::order_status (4 = cancelled). Rules and errors are as for
BcsHttpClient::cancel_order_by_id.
§Errors
The same as BcsHttpClient::cancel_order_by_id.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CancelByIdRequest, CreateOrderRequest};
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
let state = client
.cancel_order_by_id_sync(&CancelByIdRequest::by_client_order_id(cid, Uuid::new_v4()))
.await?;
assert_eq!(state.data[0].order_status.as_deref(), Some("4")); // 4 = cancelledSourcepub async fn edit_order(
&self,
request: &EditOrderRequest,
) -> Result<OrderOperationResponse, BcsApiError>
pub async fn edit_order( &self, request: &EditOrderRequest, ) -> Result<OrderOperationResponse, BcsApiError>
Changes an order: POST .../orders/edit.
The broker does the change as a cancel of the original and a new order (broker docs). So the
client_order_id of the request is the id of the new order, and the original ends in status
“replaced” (5, by the status table of the docs, not verified against the live API). Orders in
status new (0), partly filled (1), replaced (5) and waiting for confirmation (10) can be changed. The reply only confirms that the request was taken.
Because it creates an order, an edit that ended without an answer has an unknown outcome and must not be repeated before you look at the orders.
§Errors
As for BcsHttpClient::create_order, plus BcsApiError::NotFound for an unknown original
order (not verified against the live API).
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CreateOrderRequest, EditOrderRequest, OrderExtras};
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
let new_cid = Uuid::new_v4();
let ack = client
.edit_order(&EditOrderRequest {
order_id_type: Some("1".into()),
order_id: cid.to_string(),
client_order_id: new_cid,
order_type: None,
price: Some(dec!(301.0)),
order_quantity: 1,
extras: OrderExtras::default(),
})
.await?;
assert_eq!(ack.client_order_id, new_cid);Sourcepub async fn edit_order_sync(
&self,
request: &EditOrderRequest,
) -> Result<OrderStateListResponse, BcsApiError>
pub async fn edit_order_sync( &self, request: &EditOrderRequest, ) -> Result<OrderStateListResponse, BcsApiError>
Changes an order and waits for the result: POST .../orders/edit/sync.
The reply is the state of the new order, in the format of BcsHttpClient::get_order.
Everything else is as for BcsHttpClient::edit_order.
§Errors
The same as BcsHttpClient::edit_order.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CreateOrderRequest, EditOrderRequest, OrderExtras};
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
let state = client
.edit_order_sync(&EditOrderRequest {
order_id_type: None, // default: the id is the clientOrderId of the original
order_id: cid.to_string(),
client_order_id: Uuid::new_v4(),
order_type: None,
price: Some(dec!(301.0)),
order_quantity: 2,
extras: OrderExtras::default(),
})
.await?;
assert_eq!(state.original_client_order_id.as_deref(), Some(cid.to_string().as_str()));Sourcepub async fn get_order(
&self,
order_id_type: &str,
order_id: &str,
) -> Result<OrderStateListResponse, BcsApiError>
pub async fn get_order( &self, order_id_type: &str, order_id: &str, ) -> Result<OrderStateListResponse, BcsApiError>
Gets the current state of one order: GET .../orders?orderIdType=&orderId=.
order_id_type is "1" for your clientOrderId (the broker default) or "2" for the
exchange order id (format YYMMDD-ClassCode-OrderNumber). It works for orders of any
supported type. The reply holds a list of reports. The docs do not say how many reports the list
has, so read the last one if you need the latest state.
This is the method to ask after a placement that timed out. BcsApiError::NotFound (HTTP 404)
means the broker has no such order.
§Errors
BcsApiError::NotFound if there is no such order. See BcsApiError for the other variants.
§Examples
use brokapi_bcs_protocol::error::BcsApiError;
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::CreateOrderRequest;
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let cid = Uuid::new_v4();
client
.create_order_sync(&CreateOrderRequest::limit(cid, "SBER", "TQBR", OrderSide::Buy, 1, dec!(300.0)))
.await?;
let state = client.get_order("1", &cid.to_string()).await?;
assert_eq!(state.data[0].executed_quantity, Some(dec!(0.0)));
// An id the broker does not know.
let missing = client.get_order("1", &Uuid::new_v4().to_string()).await;
assert!(matches!(missing, Err(BcsApiError::NotFound { .. })));Sourcepub async fn get_order_status(
&self,
original_client_order_id: Uuid,
) -> Result<OrderStatusResponse, BcsApiError>
pub async fn get_order_status( &self, original_client_order_id: Uuid, ) -> Result<OrderStatusResponse, BcsApiError>
Gets the state of an order, the deprecated form with the id in the path: GET .../orders/{id}.
The broker docs list this method in their deprecated section. Use BcsHttpClient::get_order.
The in-process imitation does not serve the deprecated forms, so this method has no runnable
example. Unlike the current form, data of the reply is a single object.
§Errors
BcsApiError::NotFound if there is no such order. See BcsApiError for the other variants.
Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn get_portfolio(&self) -> Result<PortfolioResponse, BcsApiError>
pub async fn get_portfolio(&self) -> Result<PortfolioResponse, BcsApiError>
Gets the portfolio: one record per position and per currency balance (GET .../portfolio).
The list holds positions of several kinds, told apart by PortfolioPosition::position_type.
Many fields are absent for some kinds, so they are all optional. The same data streams on the
portfolio WebSocket channel, see crate::ws::client::BcsWsClient::connect_portfolio.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
let client = BcsHttpClient::new(cfg)?;
let positions = client.get_portfolio().await?;
assert!(!positions.is_empty());Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn search_trades(
&self,
req: &TradeSearchRequest,
size: Option<u32>,
page: Option<u32>,
) -> Result<PaginatedResponse<TradeRecord>, BcsApiError>
pub async fn search_trades( &self, req: &TradeSearchRequest, size: Option<u32>, page: Option<u32>, ) -> Result<PaginatedResponse<TradeRecord>, BcsApiError>
Lists your own trades (fills) by filter: POST .../trades/search.
The list holds trades made since 2026-01-26 and at most the 1000 latest (broker docs). size
is 1 to 100 (default 50), page starts at 0. A record has the exchange order number and the
trade number but not your clientOrderId, and no commission.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::{CreateOrderRequest, TradeSearchRequest};
use brokapi_bcs_protocol::models::enums::OrderSide;
use uuid::Uuid;
let client = BcsHttpClient::new(cfg)?;
let trades = client
.search_trades(
&TradeSearchRequest {
start_date_time: "2026-01-26T00:00:00.000Z".into(),
end_date_time: "2099-01-01T00:00:00.000Z".into(),
side: None,
tickers: Some(vec!["SBER".into()]),
},
Some(50),
Some(0),
)
.await?;
assert_eq!(trades.records.len(), 1);
assert_eq!(trades.records[0].price, Some(dec!(300.0)));Source§impl BcsHttpClient
impl BcsHttpClient
Sourcepub async fn search_operations(
&self,
req: &OperationHistoryRequest,
size: Option<u32>,
page: Option<u32>,
) -> Result<OperationHistoryResponse, BcsApiError>
pub async fn search_operations( &self, req: &OperationHistoryRequest, size: Option<u32>, page: Option<u32>, ) -> Result<OperationHistoryResponse, BcsApiError>
Lists non-trade operations: deposits, withdrawals, dividends, coupons, commissions, taxes
(POST .../operations/search).
All filters in the request are optional. size (1 to 100, default 50) and page (from 0) are
sent in the query string. The broker docs mark them as path parameters, but the path of the
method has none (the same as for the trades search), so the query is an assumption that is not
verified against the live API. This service has the tightest rate limit of the API, 3 requests
per second.
§Errors
See BcsApiError.
§Examples
use brokapi_bcs_protocol::http::client::BcsHttpClient;
use brokapi_bcs_protocol::http::models::OperationHistoryRequest;
let client = BcsHttpClient::new(cfg)?;
let history = client
.search_operations(
&OperationHistoryRequest {
operation_types: Some(vec!["PayIn".into()]),
..Default::default()
},
Some(20),
Some(0),
)
.await?;
assert_eq!(history.records[0].operation_type.as_deref(), Some("PayIn"));Trait Implementations§
Source§impl Clone for BcsHttpClient
impl Clone for BcsHttpClient
Source§fn clone(&self) -> BcsHttpClient
fn clone(&self) -> BcsHttpClient
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more