Skip to main content

BcsHttpClient

Struct BcsHttpClient 

Source
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_in is 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

Source

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");
Source

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);
Source

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. message is its error_description (or error).
  • 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());
Source

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
§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), .. }));
Source

pub async fn get_with_query<T, Q>( &self, path: &str, query: &Q, ) -> Result<T, BcsApiError>

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");
Source

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"));
Source

pub async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, BcsApiError>

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());
Source

pub async fn post_command<T, B>( &self, path: &str, body: &B, ) -> Result<T, BcsApiError>

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.

Source

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();
Source

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");
Source

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");
Source

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

Source

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

Source

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());
Source

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");
Source

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");
Source

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");
Source

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

Source

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

Source

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

Source

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 { .. }));
Source

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));
Source

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)));
Source

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 = &quotes.records[0];
assert!(sber.bid.is_some() && sber.offer.is_some());
Source§

impl BcsHttpClient

Source

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);
Source

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 orders
Source

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
§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 { .. })));
Source

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.

Source

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.

Source

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"));
Source

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
§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");
Source

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 = cancelled
Source

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);
Source

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()));
Source

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 { .. })));
Source

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

Source

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

Source

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

Source

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

Source§

fn clone(&self) -> BcsHttpClient

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more