brokapi_bcs_protocol/http/models.rs
1//! Request and response models of the REST API, one struct per JSON shape of the broker.
2//!
3//! Field names are in Rust style and map to the broker's camelCase names through `serde`. Wherever the
4//! broker docs say a value is one of a closed set (side, status, instrument type), these models keep
5//! it as text when a new value must not break parsing, and use an enum when the set is stable. Almost
6//! every field of a reply is an `Option`, because the broker fills only what fits the object. Check
7//! before you use a field.
8//!
9//! All dates and times from the broker are UTC (broker docs). Quantities of orders are in pieces.
10
11use crate::models::enums::OrderSide;
12use crate::utils::date_format;
13use chrono::{DateTime, Utc};
14use rust_decimal::Decimal;
15use serde::{Deserialize, Serialize};
16use uuid::Uuid;
17
18// ─── auth ─────────────────────────────────────────────────────────────────────
19
20/// The form the client sends to the token service to trade a refresh token for an access token.
21///
22/// The client builds and sends it for you. It is public so you can see the exact wire form. It is sent as
23/// `application/x-www-form-urlencoded`.
24///
25/// # Examples
26///
27/// ```
28/// use brokapi_bcs_protocol::http::models::TokenRequest;
29///
30/// let form = TokenRequest {
31/// grant_type: "refresh_token".into(),
32/// refresh_token: "my-refresh-token".into(),
33/// client_id: "trade-api-read".into(),
34/// };
35/// assert_eq!(
36/// serde_urlencoded::to_string(&form)?,
37/// "grant_type=refresh_token&refresh_token=my-refresh-token&client_id=trade-api-read"
38/// );
39/// # Ok::<(), serde_urlencoded::ser::Error>(())
40/// ```
41#[derive(Clone, Serialize)]
42pub struct TokenRequest {
43 /// Always `refresh_token` here: the OAuth grant that renews a token.
44 pub grant_type: String,
45 /// The refresh token. A secret.
46 pub refresh_token: String,
47 /// `trade-api-read` for a read token, `trade-api-write` for a trading token.
48 pub client_id: String,
49}
50
51impl std::fmt::Debug for TokenRequest {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("TokenRequest")
54 .field("grant_type", &self.grant_type)
55 .field("refresh_token", &"<redacted>")
56 .field("client_id", &self.client_id)
57 .finish()
58 }
59}
60
61/// The answer of the token service (OAuth style).
62///
63/// The client reads it to get the new tokens. [`BcsHttpClient::authenticate`](crate::http::client::BcsHttpClient::authenticate)
64/// returns a value of this type, but builds it locally, see there.
65///
66/// # Examples
67///
68/// ```
69/// use brokapi_bcs_protocol::http::models::TokenResponse;
70///
71/// let reply = r#"{"access_token":"a","expires_in":86400,"refresh_expires_in":7776000,
72/// "refresh_token":"r","token_type":"bearer"}"#;
73/// let token: TokenResponse = serde_json::from_str(reply)?;
74/// assert_eq!(token.expires_in, 86_400); // 24 hours
75/// assert_eq!(token.refresh_token.as_deref(), Some("r"));
76/// # Ok::<(), serde_json::Error>(())
77/// ```
78#[derive(Clone, Deserialize)]
79pub struct TokenResponse {
80 /// The access token to put in `Authorization: Bearer ...`. A secret.
81 pub access_token: String,
82 /// Lifetime of the access token in seconds. The broker docs say 24 hours in total.
83 pub expires_in: u64,
84 /// Lifetime of the refresh token in seconds. The broker docs say 90 days in total.
85 pub refresh_expires_in: u64,
86 /// A new refresh token, if the token service rotates it. Keep it in place of the old one. `None` if the reply carried none.
87 pub refresh_token: Option<String>,
88 /// Type of the access token, `bearer` by the token service.
89 pub token_type: String,
90 /// Session id of the token service, if it sends one.
91 #[serde(default)]
92 pub session_state: Option<String>,
93 /// Scope granted to the token, if the token service sends one.
94 #[serde(default)]
95 pub scope: Option<String>,
96}
97
98impl std::fmt::Debug for TokenResponse {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("TokenResponse")
101 .field("access_token", &"<redacted>")
102 .field("expires_in", &self.expires_in)
103 .field("refresh_expires_in", &self.refresh_expires_in)
104 .field(
105 "refresh_token",
106 &self.refresh_token.as_ref().map(|_| "<redacted>"),
107 )
108 .field("token_type", &self.token_type)
109 .field("session_state", &self.session_state)
110 .field("scope", &self.scope)
111 .finish()
112 }
113}
114
115// ─── common ───────────────────────────────────────────────────────────────────
116
117/// A page of a list, with the totals the broker reports.
118///
119/// Pages count from 0. `total_records` and `total_pages` are for the whole result of the filter, not for
120/// this page. The lists of orders and trades hold at most the 1000 latest records (broker docs).
121///
122/// # Examples
123///
124/// ```
125/// use brokapi_bcs_protocol::http::models::PaginatedResponse;
126///
127/// let page: PaginatedResponse<String> =
128/// serde_json::from_str(r#"{"records":["a","b"],"totalRecords":2,"totalPages":1}"#)?;
129/// assert_eq!((page.records.len(), page.total_pages), (2, 1));
130/// # Ok::<(), serde_json::Error>(())
131/// ```
132#[derive(Debug, Clone, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct PaginatedResponse<T> {
135 /// The records of this page.
136 pub records: Vec<T>,
137 /// Number of records for the whole filter.
138 pub total_records: u64,
139 /// Number of pages for the whole filter, at the page size of the request.
140 pub total_pages: u64,
141}
142
143/// A generic acknowledgement of a command: the id of the request and a status text.
144///
145/// No client method returns it. [`OrderOperationResponse`] is the one the order methods use.
146#[derive(Debug, Clone, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct OperationResponse {
149 /// Id of the request, as text.
150 pub client_order_id: String,
151 /// Status text of the broker.
152 pub status: String,
153}
154
155// ─── information ──────────────────────────────────────────────────────────────
156
157/// Parameters of [`BcsHttpClient::get_daily_schedule`](crate::http::client::BcsHttpClient::get_daily_schedule).
158///
159/// Both fields are required by the broker. Sent as a query string.
160#[derive(Debug, Clone)]
161pub struct DailyScheduleRequest {
162 /// Class code (board), for example `TQBR`.
163 pub class_code: String,
164 /// Ticker, for example `SBER`.
165 pub ticker: String,
166}
167
168/// One session of the day in a daily schedule.
169///
170/// # Examples
171///
172/// ```
173/// use brokapi_bcs_protocol::http::models::ScheduleEntry;
174///
175/// let e: ScheduleEntry = serde_json::from_str(
176/// r#"{"startDate":"10:00:00","endDate":"18:50:00","tradingSessionType":"Торговый период","tradingSessionStatus":"OPEN"}"#,
177/// )?;
178/// assert_eq!(e.trading_session_status, "OPEN");
179/// # Ok::<(), serde_json::Error>(())
180/// ```
181#[derive(Debug, Clone, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct ScheduleEntry {
184 /// Start of the session as a time of day, `HH:MM:SS`. Despite the name it is not a date. The time zone is not stated in the broker docs (not verified against the live API).
185 pub start_date: String,
186 /// End of the session as a time of day, `HH:MM:SS`. The time zone is not stated in the broker docs.
187 pub end_date: String,
188 /// Name of the session, in Russian. The docs list: technical break of QUIK, opening auction, non-working time or day off or holiday, evening session, trading period, closing auction.
189 pub trading_session_type: String,
190 /// `OPEN` or `CLOSE`.
191 pub trading_session_status: String,
192}
193
194/// The trading schedule of one instrument for the day.
195#[derive(Debug, Clone, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct DailyScheduleResponse {
198 /// `true` if the day is a trading day.
199 pub is_work_day: bool,
200 /// The sessions of the day, with their status.
201 pub daily_schedule: Vec<ScheduleEntry>,
202}
203
204/// Body of [`BcsHttpClient::get_instruments_by_isins`](crate::http::client::BcsHttpClient::get_instruments_by_isins).
205///
206/// # Examples
207///
208/// ```
209/// use brokapi_bcs_protocol::http::models::InstrumentsByIsinsRequest;
210///
211/// let body = serde_json::to_string(&InstrumentsByIsinsRequest { isins: vec!["RU0009029540".into()] })?;
212/// assert_eq!(body, r#"{"isins":["RU0009029540"]}"#);
213/// # Ok::<(), serde_json::Error>(())
214/// ```
215#[derive(Debug, Clone, Serialize)]
216pub struct InstrumentsByIsinsRequest {
217 /// ISIN codes to look up. The broker wants at least one, all different.
218 pub isins: Vec<String>,
219}
220
221/// Body of [`BcsHttpClient::get_instruments_by_tickers`](crate::http::client::BcsHttpClient::get_instruments_by_tickers).
222///
223/// # Examples
224///
225/// ```
226/// use brokapi_bcs_protocol::http::models::InstrumentsByTickersRequest;
227///
228/// let body = serde_json::to_string(&InstrumentsByTickersRequest { tickers: vec!["SBER".into()] })?;
229/// assert_eq!(body, r#"{"tickers":["SBER"]}"#);
230/// # Ok::<(), serde_json::Error>(())
231/// ```
232#[derive(Debug, Clone, Serialize)]
233pub struct InstrumentsByTickersRequest {
234 /// Tickers to look up. The broker wants at least one, all different.
235 pub tickers: Vec<String>,
236}
237
238/// Parameters of [`BcsHttpClient::get_instruments_by_type`](crate::http::client::BcsHttpClient::get_instruments_by_type).
239///
240/// Sent as a query string. Fields left as `None` are not sent.
241#[derive(Debug, Clone)]
242pub struct InstrumentsByTypeRequest {
243 /// Instrument type in the broker spelling: `CURRENCY`, `STOCK`, `FOREIGN_STOCK`, `BONDS`, `NOTES`, `DEPOSITARY_RECEIPTS`, `EURO_BONDS`, `MUTUAL_FUNDS`, `ETF`, `FUTURES`, `OPTIONS`, `GOODS` or `INDICES`. Another spelling (for example `Stock`) is refused.
244 pub instrument_type: String,
245 /// Ticker of the base asset. The broker requires it when the type is `OPTIONS`.
246 pub base_asset_ticker: Option<String>,
247 /// Page size, 1 to 100. The broker default is 50.
248 pub size: Option<u32>,
249 /// Page number, from 0. The broker default is 0.
250 pub page: Option<u32>,
251}
252
253/// Parameters of [`BcsHttpClient::get_trading_status`](crate::http::client::BcsHttpClient::get_trading_status).
254#[derive(Debug, Clone)]
255pub struct TradingStatusRequest {
256 /// Class code (board), for example `TQBR`. Required.
257 pub class_code: String,
258}
259
260/// The trading session status of a board.
261///
262/// # Examples
263///
264/// ```
265/// use brokapi_bcs_protocol::http::models::TradingStatusResponse;
266///
267/// let s: TradingStatusResponse = serde_json::from_str(
268/// r#"{"tradingSessionTypeId":1,"tradingSessionType":"MAIN","tradingSessionStatus":"OPEN",
269/// "nextSessionDate":"2026-10-20T07:00:00.000Z"}"#,
270/// )?;
271/// assert_eq!(s.trading_session_status, "OPEN");
272/// # Ok::<(), serde_json::Error>(())
273/// ```
274#[derive(Debug, Clone, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct TradingStatusResponse {
277 /// Id of the current session.
278 pub trading_session_type_id: i32,
279 /// Name of the current session.
280 pub trading_session_type: String,
281 /// `OPEN` or `CLOSE`.
282 pub trading_session_status: String,
283 /// Date and time (UTC) of the next change of the status, as the broker docs describe it.
284 #[serde(with = "date_format")]
285 pub next_session_date: DateTime<Utc>,
286}
287
288/// Reference data of one instrument, as the information service returns it.
289///
290/// Only `ticker` is always there. Every other field is optional, because the broker fills the ones that fit
291/// the instrument type (a coupon rate only for a bond, a base asset only for a derivative). Do not trade
292/// on a default: see [`InstrumentInfo::trading_params_for_board`] for what is assumed when a field is missing.
293///
294/// # Examples
295///
296/// ```
297/// # use rust_decimal::Decimal;
298/// # use rust_decimal_macros::dec;
299/// use brokapi_bcs_protocol::http::models::InstrumentInfo;
300///
301/// let info: InstrumentInfo = serde_json::from_str(
302/// r#"{"ticker":"SBER","boards":[{"classCode":"TQBR","exchange":"MOEX"}],"lotSize":1,
303/// "minimumStep":0.01,"scale":2,"tradingCurrency":"RUB","primaryBoard":"TQBR"}"#,
304/// )?;
305/// let params = info.trading_params_primary().expect("a board");
306/// assert_eq!(params.class_code, "TQBR");
307/// assert_eq!(params.price_step, dec!(0.01));
308/// # Ok::<(), serde_json::Error>(())
309/// ```
310#[derive(Debug, Clone, Deserialize, Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct InstrumentInfo {
313 /// Ticker of the instrument.
314 pub ticker: String,
315 /// The boards (class codes and exchanges) where the instrument trades.
316 #[serde(default)]
317 pub boards: Vec<InstrumentBoard>,
318 /// Short name.
319 #[serde(default)]
320 pub short_name: Option<String>,
321 /// Name for display.
322 #[serde(default)]
323 pub display_name: Option<String>,
324 /// Instrument type in the broker spelling (JSON field `type`): `CURRENCY`, `STOCK`, `FOREIGN_STOCK`, `BONDS`, `NOTES`, `DEPOSITARY_RECEIPTS`, `EURO_BONDS`, `MUTUAL_FUNDS`, `ETF`, `FUTURES`, `OPTIONS`, `GOODS`, `INDICES`.
325 #[serde(rename = "type", default)]
326 pub instrument_type_name: Option<String>,
327 /// ISIN code.
328 #[serde(default)]
329 pub isin: Option<String>,
330 /// State registration code of the issue.
331 #[serde(default)]
332 pub registration_code: Option<String>,
333 /// Name of the issuer.
334 #[serde(default)]
335 pub issuer_name: Option<String>,
336 /// Currency of the instrument (the trading currency).
337 #[serde(default)]
338 pub trading_currency: Option<String>,
339 /// Face value (nominal), for bonds.
340 #[serde(default)]
341 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
342 pub face_value: Option<Decimal>,
343 /// Precision: the number of decimal places of the price.
344 #[serde(default)]
345 pub scale: Option<u32>,
346 /// Minimum price step. A price you send must be a whole number of steps.
347 #[serde(default)]
348 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
349 pub minimum_step: Option<Decimal>,
350 /// Accrued coupon income at the settlement date.
351 #[serde(default)]
352 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
353 pub accrued_int: Option<Decimal>,
354 /// Currency of the price step value.
355 #[serde(default)]
356 pub currency_step_price: Option<String>,
357 /// Default settlement code.
358 #[serde(default)]
359 pub settle_code: Option<String>,
360 /// Instrument type in the broker spelling (JSON field `instrumentType`). Same values as [`InstrumentInfo::instrument_type_name`].
361 #[serde(default)]
362 pub instrument_type: Option<String>,
363 /// Settlement currency.
364 #[serde(default)]
365 pub settlement_currency: Option<String>,
366 /// Settlement date, kept as the text the broker sends (a date-time).
367 #[serde(default)]
368 pub settlement_date: Option<String>,
369 /// Maturity (redemption) date, kept as the text the broker sends.
370 #[serde(default)]
371 pub maturity_date: Option<String>,
372 /// Size of a lot: pieces in one lot. Order quantities of the API are in pieces, not lots.
373 #[serde(default)]
374 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
375 pub lot_size: Option<Decimal>,
376 /// `true` for an instrument only for qualified investors.
377 #[serde(default)]
378 pub is_qualified_only: Option<bool>,
379 /// `true` if a short sale is available.
380 #[serde(default)]
381 pub is_can_short: Option<bool>,
382 /// Name of the base asset of a derivative contract.
383 #[serde(default)]
384 pub base_asset: Option<String>,
385 /// `true` if an unqualified investor may trade the instrument.
386 #[serde(default)]
387 pub available_for_unqualified: Option<bool>,
388 /// Cost of one price step, in the currency of the step price.
389 #[serde(default)]
390 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
391 pub step_price: Option<Decimal>,
392 /// `true` for a product of the broker itself.
393 #[serde(default)]
394 pub is_bcs_product: Option<bool>,
395 /// Link to a logo. Deprecated by the broker.
396 #[serde(default)]
397 pub logo_link: Option<String>,
398 /// Number of coupons per year, for bonds.
399 #[serde(default)]
400 pub coupons_per_year: Option<u32>,
401 /// Coupon rate in percent per year, for bonds.
402 #[serde(default)]
403 pub coupon_rate: Option<f64>,
404 /// Date of the next coupon payment, kept as the text the broker sends.
405 #[serde(default)]
406 pub next_coupon: Option<String>,
407 /// Marking of a complex product by the knowledge tests (the broker docs call it "marking by tests").
408 #[serde(default)]
409 pub complex_product: Option<f64>,
410 /// Type of the base asset, for futures.
411 #[serde(default)]
412 pub base_asset_future: Option<String>,
413 /// Name of the instrument subtype.
414 #[serde(default)]
415 pub sub_type: Option<String>,
416 /// Business sector of the issuer.
417 #[serde(default)]
418 pub business_sector: Option<String>,
419 /// Dividend yield: the stated annual dividend divided by the closing price.
420 #[serde(default)]
421 pub dividend_yield: Option<f64>,
422 /// Analyst target price.
423 #[serde(default)]
424 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
425 pub target_price: Option<Decimal>,
426 /// Market capitalisation.
427 #[serde(default)]
428 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
429 pub mktcap: Option<Decimal>,
430 /// `true` if the class is blocked for trading.
431 #[serde(default)]
432 pub is_blocked: Option<bool>,
433 /// Class code of the primary board. Empty if the broker did not send it.
434 #[serde(default)]
435 pub primary_board: String,
436}
437
438/// A board an instrument trades on.
439#[derive(Debug, Clone, Deserialize, Serialize)]
440#[serde(rename_all = "camelCase")]
441pub struct InstrumentBoard {
442 /// Class code of the board, for example `TQBR`.
443 pub class_code: String,
444 /// Exchange code, for example `MOEX`.
445 pub exchange: String,
446}
447
448/// Normalized trading parameters extracted from `InstrumentInfo` for domain mapping.
449#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct TradingParamsSnapshot {
452 /// Ticker.
453 pub ticker: String,
454 /// Class code (board) the parameters are for.
455 pub class_code: String,
456 /// Pieces in one lot. `1` if the broker sent none (a default, not data).
457 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
458 pub lot_size: Decimal,
459 /// Minimum price step. `0.01` if the broker sent none (a default, not data).
460 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
461 pub price_step: Decimal,
462 /// Cost of one price step. If the broker sent none it is the price step (a default).
463 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
464 pub price_step_value: Decimal,
465 /// Decimal places of the price. `2` if the broker sent none (a default).
466 pub scale: u32,
467 /// Trading currency. `RUB` if the broker sent none (a default).
468 pub currency: String,
469}
470
471impl InstrumentInfo {
472 /// Builds [`TradingParamsSnapshot`] for one board (class code).
473 ///
474 /// Returns `None` when the instrument lists boards and `class_code` is not one of them. If the
475 /// instrument lists no boards at all, any `class_code` is accepted.
476 ///
477 /// **A missing field is replaced by a default, not reported:** lot size 1, price step 0.01, price
478 /// step value equal to the step, scale 2, currency `RUB`. The defaults are a guess. Check the
479 /// fields of [`InstrumentInfo`] itself before you trade on them.
480 ///
481 /// # Examples
482 ///
483 /// ```
484 /// # use rust_decimal::Decimal;
485 /// # use rust_decimal_macros::dec;
486 /// use brokapi_bcs_protocol::http::models::InstrumentInfo;
487 ///
488 /// let info: InstrumentInfo = serde_json::from_str(
489 /// r#"{"ticker":"SBER","boards":[{"classCode":"TQBR","exchange":"MOEX"}],"lotSize":10,"minimumStep":0.05}"#,
490 /// )?;
491 /// let p = info.trading_params_for_board("TQBR").expect("listed board");
492 /// assert_eq!((p.lot_size, p.price_step, p.scale), (dec!(10.0), dec!(0.05), 2)); // scale 2 is a default
493 /// assert!(info.trading_params_for_board("SMAL").is_none()); // not a board of this instrument
494 /// # Ok::<(), serde_json::Error>(())
495 /// ```
496 pub fn trading_params_for_board(&self, class_code: &str) -> Option<TradingParamsSnapshot> {
497 if !self.boards.is_empty() && !self.boards.iter().any(|b| b.class_code == class_code) {
498 return None;
499 }
500
501 Some(TradingParamsSnapshot {
502 ticker: self.ticker.clone(),
503 class_code: class_code.to_string(),
504 lot_size: self.lot_size.unwrap_or(Decimal::ONE),
505 price_step: self.minimum_step.unwrap_or(Decimal::new(1, 2)),
506 price_step_value: self
507 .step_price
508 .unwrap_or_else(|| self.minimum_step.unwrap_or(Decimal::new(1, 2))),
509 scale: self.scale.unwrap_or(2),
510 currency: self
511 .trading_currency
512 .clone()
513 .unwrap_or_else(|| "RUB".to_string()),
514 })
515 }
516
517 /// Builds [`TradingParamsSnapshot`] for the primary board of the instrument.
518 ///
519 /// The primary board is [`InstrumentInfo::primary_board`], or the first listed board if that is
520 /// empty. Returns `None` if there is neither. The defaults of
521 /// [`InstrumentInfo::trading_params_for_board`] apply.
522 ///
523 /// # Examples
524 ///
525 /// ```
526 /// use brokapi_bcs_protocol::http::models::InstrumentInfo;
527 ///
528 /// let info: InstrumentInfo = serde_json::from_str(r#"{"ticker":"SBER","primaryBoard":"TQBR"}"#)?;
529 /// assert_eq!(info.trading_params_primary().unwrap().class_code, "TQBR");
530 ///
531 /// let nothing: InstrumentInfo = serde_json::from_str(r#"{"ticker":"X"}"#)?;
532 /// assert!(nothing.trading_params_primary().is_none());
533 /// # Ok::<(), serde_json::Error>(())
534 /// ```
535 pub fn trading_params_primary(&self) -> Option<TradingParamsSnapshot> {
536 let class_code = if self.primary_board.is_empty() {
537 self.boards.first()?.class_code.clone()
538 } else {
539 self.primary_board.clone()
540 };
541 self.trading_params_for_board(&class_code)
542 }
543}
544
545// ─── limits ───────────────────────────────────────────────────────────────────
546
547/// A position record with the fields of a portfolio position.
548///
549/// No client method returns it. Use [`LimitsResponse`] for the limits and [`PortfolioPosition`] for positions.
550#[derive(Debug, Clone, Deserialize)]
551#[serde(rename_all = "camelCase")]
552pub struct LimitPosition {
553 /// Id of the sub-account. Deprecated by the broker.
554 #[serde(default)]
555 pub sub_account_id: Option<String>,
556 /// Id of the general agreement. Deprecated by the broker.
557 #[serde(default)]
558 pub agreement_id: Option<String>,
559 /// Trading account.
560 #[serde(default)]
561 pub account: Option<String>,
562 /// Exchange code.
563 #[serde(default)]
564 pub exchange: Option<String>,
565 /// Ticker.
566 #[serde(default)]
567 pub ticker: Option<String>,
568 /// Name of the instrument for display.
569 #[serde(default)]
570 pub display_name: Option<String>,
571 /// Price currency.
572 #[serde(default)]
573 pub currency: Option<String>,
574 /// Instrument type, as text.
575 #[serde(default)]
576 pub instrument_type: Option<String>,
577 /// Settlement term code.
578 #[serde(default)]
579 pub term: Option<String>,
580 /// Quantity in pieces.
581 #[serde(default)]
582 pub quantity: Option<Decimal>,
583 /// Quantity held by active orders or by collateral (initial margin).
584 #[serde(default)]
585 pub locked: Option<Decimal>,
586 /// Balance price (the price the position was opened at), in the price currency.
587 #[serde(default)]
588 pub balance_price: Option<Decimal>,
589 /// Current price.
590 #[serde(default)]
591 pub current_price: Option<Decimal>,
592 /// Balance value (at the opening price), in the price currency.
593 #[serde(default)]
594 pub balance_value: Option<Decimal>,
595 /// Balance value in roubles.
596 #[serde(default)]
597 pub balance_value_rub: Option<Decimal>,
598 /// Current value in the price currency.
599 #[serde(default)]
600 pub current_value: Option<Decimal>,
601 /// Current value in roubles.
602 #[serde(default)]
603 pub current_value_rub: Option<Decimal>,
604 /// Price change since the position was opened (JSON field `unrealizedPL`, with capital letters).
605 #[serde(default, rename = "unrealizedPL", alias = "unrealizedPl")]
606 pub unrealized_pl: Option<Decimal>,
607 /// The same change in percent (JSON field `unrealizedPercentPL`).
608 #[serde(default, rename = "unrealizedPercentPL", alias = "unrealizedPercentPl")]
609 pub unrealized_percent_pl: Option<f64>,
610 /// Price change over the day (JSON field `dailyPL`). The broker docs word it as "daily change of the price since the position was opened".
611 #[serde(default, rename = "dailyPL", alias = "dailyPl")]
612 pub daily_pl: Option<Decimal>,
613 /// The same change in percent (JSON field `dailyPercentPL`).
614 #[serde(default, rename = "dailyPercentPL", alias = "dailyPercentPl")]
615 pub daily_percent_pl: Option<f64>,
616 /// Share of the portfolio in percent, of its value in roubles.
617 #[serde(default)]
618 pub portfolio_share: Option<f64>,
619 /// `true` if the class is blocked.
620 #[serde(default)]
621 pub is_blocked: Option<bool>,
622 /// `true` if the trading account is blocked.
623 #[serde(default)]
624 pub is_blocked_trade_account: Option<bool>,
625}
626
627/// The account limits: positions, money and futures limits.
628///
629/// # Examples
630///
631/// ```
632/// use brokapi_bcs_protocol::http::models::LimitsResponse;
633///
634/// let limits: LimitsResponse = serde_json::from_str(
635/// r#"{"moneyLimits":[{"currencyCode":"RUB","locked":10.0,"quantity":{"type":"T0","value":500.0}}]}"#,
636/// )?;
637/// assert!(limits.depo_limit.is_empty()); // a list the broker left out reads as empty
638/// assert_eq!(limits.money_limits[0].currency.as_deref(), Some("RUB"));
639/// # Ok::<(), serde_json::Error>(())
640/// ```
641#[derive(Debug, Clone, Deserialize)]
642#[serde(rename_all = "camelCase")]
643pub struct LimitsResponse {
644 /// Securities positions, one record per instrument and settlement mode.
645 #[serde(default)]
646 pub depo_limit: Vec<DepoLimit>,
647 /// Money balances, one record per currency and settlement mode.
648 #[serde(default)]
649 pub money_limits: Vec<MoneyLimit>,
650 /// Limits of the derivatives market.
651 #[serde(default)]
652 pub futures_limits: Vec<FuturesLimit>,
653}
654
655/// A securities position in the limits.
656#[derive(Debug, Clone, Deserialize)]
657#[serde(rename_all = "camelCase")]
658pub struct DepoLimit {
659 /// Ticker.
660 #[serde(default)]
661 pub ticker: Option<String>,
662 /// Class code (board).
663 #[serde(default)]
664 pub class_code: Option<String>,
665 /// Exchange code.
666 #[serde(default)]
667 pub exchange: Option<String>,
668 /// Weighted average price at which the position was opened.
669 #[serde(default)]
670 pub average_price: Option<Decimal>,
671 /// Quantity in pieces, with the settlement mode.
672 pub quantity: Option<Quantity>,
673 /// Quantity in lots, with the settlement mode.
674 pub quantity_batch: Option<Quantity>,
675 /// Instrument type as the broker names it.
676 #[serde(default)]
677 pub instrument_type: Option<String>,
678}
679
680/// A money balance in the limits.
681#[derive(Debug, Clone, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct MoneyLimit {
684 /// Exchange code.
685 #[serde(default)]
686 pub exchange: Option<String>,
687 /// Currency code (JSON field `currencyCode`).
688 #[serde(default, rename = "currencyCode")]
689 pub currency: Option<String>,
690 /// Money held by active orders.
691 #[serde(default)]
692 pub locked: Option<Decimal>,
693 /// Weighted average opening price.
694 #[serde(default)]
695 pub average_price: Option<Decimal>,
696 /// Instrument type as the broker names it.
697 #[serde(default)]
698 pub instrument_type: Option<String>,
699 /// Amount of money, with the settlement mode.
700 pub quantity: Option<Quantity>,
701 /// When the broker last updated the record, kept as text.
702 #[serde(default)]
703 pub load_date: Option<String>,
704}
705
706/// A limit of the derivatives market (collateral and variation margin).
707#[derive(Debug, Clone, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct FuturesLimit {
710 /// Currency code (JSON field `currencyCode`).
711 #[serde(default, rename = "currencyCode")]
712 pub currency: Option<String>,
713 /// Exchange code.
714 #[serde(default)]
715 pub exchange: Option<String>,
716 /// Current limit of open positions.
717 #[serde(default)]
718 pub cbp_limit: Option<Decimal>,
719 /// Positions after the last clearing.
720 #[serde(default)]
721 pub cbpl_used: Option<Decimal>,
722 /// Estimated value of the planned positions after the nearest clearing.
723 #[serde(default)]
724 pub cbpl_planned: Option<Decimal>,
725 /// Collateral (initial margin) reserved for the client orders.
726 #[serde(default)]
727 pub cbpl_used_for_orders: Option<Decimal>,
728 /// Collateral (initial margin) reserved for the open client positions.
729 #[serde(default)]
730 pub cbpl_used_for_positions: Option<Decimal>,
731 /// Estimate of the variation margin on the positions.
732 #[serde(default)]
733 pub var_margin: Option<Decimal>,
734 /// Variation margin credited during clearing.
735 #[serde(default)]
736 pub real_var_margin: Option<Decimal>,
737 /// Variation margin for the current day (the field is spelled `accruedint` by the broker).
738 #[serde(default)]
739 pub accruedint: Option<Decimal>,
740 /// Premiums on options.
741 #[serde(default)]
742 pub options_premium: Option<Decimal>,
743 /// Instrument type, as text.
744 #[serde(default)]
745 pub instrument_type: Option<String>,
746 /// When the broker last updated the record, kept as text.
747 #[serde(default)]
748 pub load_date: Option<String>,
749}
750
751/// An amount with its settlement mode (REST limits).
752///
753/// The settlement mode is the term in which the trade settles (for example `T0`, `T1`, `T365`).
754///
755/// # Examples
756///
757/// ```
758/// # use rust_decimal::Decimal;
759/// # use rust_decimal_macros::dec;
760/// use brokapi_bcs_protocol::http::models::Quantity;
761///
762/// let q: Quantity = serde_json::from_str(r#"{"type":"T365","value":10}"#)?;
763/// assert_eq!((q.quantity_type.as_str(), q.value), ("T365", dec!(10.0)));
764/// # Ok::<(), serde_json::Error>(())
765/// ```
766#[derive(Debug, Clone, Deserialize)]
767#[serde(rename_all = "camelCase")]
768pub struct Quantity {
769 /// Settlement mode, such as `T0` (JSON field `type`).
770 #[serde(rename = "type")]
771 pub quantity_type: String,
772 /// The amount: pieces for `quantity`, lots for `quantityBatch`, money for a money limit.
773 pub value: Decimal,
774}
775
776// ─── marginal ─────────────────────────────────────────────────────────────────
777
778/// The margin discount rates of one instrument.
779///
780/// The broker docs name the fields "rate for long" and "rate for short" and do not give the unit
781/// (fraction or percent). Not verified against the live API.
782#[derive(Debug, Clone, Deserialize)]
783#[serde(rename_all = "camelCase")]
784pub struct InstrumentDiscount {
785 /// Ticker.
786 pub ticker: String,
787 /// Rate for a long position. `0` if the broker sent none.
788 #[serde(default)]
789 pub discount_long: f64,
790 /// Rate for a short position. `0` if the broker sent none.
791 #[serde(default)]
792 pub discount_short: f64,
793}
794
795/// The answer of [`BcsHttpClient::get_instruments_discounts`](crate::http::client::BcsHttpClient::get_instruments_discounts): a list of discounts.
796pub type InstrumentDiscountsResponse = Vec<InstrumentDiscount>;
797
798// ─── market_data ──────────────────────────────────────────────────────────────
799
800/// Parameters of [`BcsHttpClient::get_candles`](crate::http::client::BcsHttpClient::get_candles).
801///
802/// Sent as a query string. The broker allows at most 1440 bars per request.
803///
804/// # Examples
805///
806/// ```
807/// use brokapi_bcs_protocol::http::models::CandlesRequest;
808/// use chrono::{TimeZone, Utc};
809///
810/// let req = CandlesRequest {
811/// ticker: "SBER".into(),
812/// class_code: "TQBR".into(),
813/// start_date: Utc.with_ymd_and_hms(2026, 10, 19, 7, 0, 0).unwrap(),
814/// end_date: Utc.with_ymd_and_hms(2026, 10, 19, 8, 0, 0).unwrap(),
815/// time_frame: "M5".into(),
816/// };
817/// assert_eq!(
818/// serde_urlencoded::to_string(&req)?,
819/// "ticker=SBER&classCode=TQBR&startDate=2026-10-19T07%3A00%3A00.000Z&endDate=2026-10-19T08%3A00%3A00.000Z&timeFrame=M5"
820/// );
821/// # Ok::<(), serde_urlencoded::ser::Error>(())
822/// ```
823#[derive(Debug, Clone, Serialize)]
824#[serde(rename_all = "camelCase")]
825pub struct CandlesRequest {
826 /// Ticker.
827 pub ticker: String,
828 /// Class code (board).
829 pub class_code: String,
830 /// Start of the period (UTC).
831 #[serde(with = "date_format")]
832 pub start_date: DateTime<Utc>,
833 /// End of the period (UTC).
834 #[serde(with = "date_format")]
835 pub end_date: DateTime<Utc>,
836 /// Time frame of a bar: `M1`, `M5`, `M15`, `M30`, `H1`, `H4`, `D`, `W` or `MN`. A string, so what you send is what the broker gets.
837 pub time_frame: String,
838}
839
840/// One candle (bar).
841#[derive(Debug, Clone, Deserialize)]
842pub struct CandleBar {
843 /// Opening time of the bar (UTC).
844 #[serde(with = "date_format")]
845 pub time: DateTime<Utc>,
846 /// Opening price.
847 pub open: Decimal,
848 /// Closing price.
849 pub close: Decimal,
850 /// Highest price.
851 pub high: Decimal,
852 /// Lowest price.
853 pub low: Decimal,
854 /// Traded volume of the bar.
855 pub volume: Decimal,
856}
857
858/// Historical candles of one instrument.
859///
860/// # Examples
861///
862/// ```
863/// # use rust_decimal::Decimal;
864/// # use rust_decimal_macros::dec;
865/// use brokapi_bcs_protocol::http::models::CandlesResponse;
866///
867/// let c: CandlesResponse = serde_json::from_str(
868/// r#"{"ticker":"SBER","classCode":"TQBR","timeFrame":"M1",
869/// "startDate":"2026-10-19T07:00:00.000Z","endDate":"2026-10-19T07:01:00.000Z",
870/// "bars":[{"time":"2026-10-19T07:00:00.000Z","open":300,"close":301,"high":302,"low":299,"volume":1200}]}"#,
871/// )?;
872/// assert_eq!(c.bars[0].close, dec!(301.0));
873/// # Ok::<(), serde_json::Error>(())
874/// ```
875#[derive(Debug, Clone, Deserialize)]
876#[serde(rename_all = "camelCase")]
877pub struct CandlesResponse {
878 /// Ticker.
879 pub ticker: String,
880 /// Class code (board).
881 pub class_code: String,
882 /// Start of the period (UTC).
883 #[serde(with = "date_format")]
884 pub start_date: DateTime<Utc>,
885 /// End of the period (UTC).
886 #[serde(with = "date_format")]
887 pub end_date: DateTime<Utc>,
888 /// Time frame of the bars, as the broker names it.
889 pub time_frame: String,
890 /// The bars of the period.
891 pub bars: Vec<CandleBar>,
892}
893
894/// Parameters of [`BcsHttpClient::get_order_book`](crate::http::client::BcsHttpClient::get_order_book).
895///
896/// Sent as a query string.
897#[derive(Debug, Clone, Serialize)]
898#[serde(rename_all = "camelCase")]
899pub struct OrderBookRequest {
900 /// Ticker.
901 pub ticker: String,
902 /// Class code (board).
903 pub class_code: String,
904 /// Levels per side, 1 to 20. The broker default is 20, this field is always sent.
905 pub depth: u32,
906}
907
908/// One price level of an order book: a price and the quantity at it.
909#[derive(Debug, Clone, Deserialize, Serialize)]
910pub struct OrderBookLevel {
911 /// Price of the level.
912 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
913 pub price: Decimal,
914 /// Quantity at the level, in pieces.
915 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
916 pub quantity: Decimal,
917}
918
919/// A snapshot of the order book.
920///
921/// `bids` run from the best (highest) price down and `asks` from the best (lowest) price up, as the broker
922/// sends them. The code does not sort them.
923///
924/// # Examples
925///
926/// ```
927/// # use rust_decimal::Decimal;
928/// # use rust_decimal_macros::dec;
929/// use brokapi_bcs_protocol::http::models::OrderBookResponse;
930///
931/// let book: OrderBookResponse = serde_json::from_str(
932/// r#"{"ticker":"SBER","classCode":"TQBR","depth":2,"bidVolume":190,"askVolume":170,
933/// "bids":[{"price":244.30,"quantity":100},{"price":244.25,"quantity":90}],
934/// "asks":[{"price":244.35,"quantity":120},{"price":244.40,"quantity":50}]}"#,
935/// )?;
936/// assert_eq!(book.bids[0].price, dec!(244.30));
937/// # Ok::<(), serde_json::Error>(())
938/// ```
939#[derive(Debug, Clone, Deserialize, Serialize)]
940#[serde(rename_all = "camelCase")]
941pub struct OrderBookResponse {
942 /// Ticker.
943 pub ticker: String,
944 /// Class code (board).
945 pub class_code: String,
946 /// Depth of the book that was returned.
947 #[serde(default)]
948 pub depth: Option<u32>,
949 /// Time the book was updated (UTC), kept as the text the broker sends.
950 #[serde(default)]
951 pub date_time: Option<String>,
952 /// Total quantity of the bids within `depth`, in pieces. It is the sum over the levels returned, not over the whole book.
953 #[serde(default)]
954 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
955 pub bid_volume: Option<Decimal>,
956 /// Total quantity of the asks within `depth`, in pieces. It is the sum over the levels returned, not over the whole book.
957 #[serde(default)]
958 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
959 pub ask_volume: Option<Decimal>,
960 /// Buy side, best price first.
961 #[serde(default)]
962 pub bids: Vec<OrderBookLevel>,
963 /// Sell side, best price first.
964 #[serde(default)]
965 pub asks: Vec<OrderBookLevel>,
966}
967
968/// `POST /api/v1/last-trades` body (schema checked 2026-10-19): the instrument, an optional period of
969/// the current session and an optional side. The API has no `limit`.
970///
971/// # Examples
972///
973/// ```
974/// use brokapi_bcs_protocol::http::models::LastTradesRequest;
975///
976/// let req = LastTradesRequest { ticker: "SBER".into(), class_code: "TQBR".into(), side: Some("1".into()), ..Default::default() };
977/// let json = serde_json::to_value(&req)?;
978/// assert_eq!(json["side"], "1");
979/// assert!(json.get("startDateTime").is_none()); // unset period is not sent
980/// # Ok::<(), serde_json::Error>(())
981/// ```
982#[derive(Debug, Clone, Default, Serialize)]
983#[serde(rename_all = "camelCase")]
984pub struct LastTradesRequest {
985 /// Ticker. Required.
986 pub ticker: String,
987 /// Class code (board). Required.
988 pub class_code: String,
989 /// Start of the period inside the current session (UTC). Left out when `None`.
990 #[serde(skip_serializing_if = "Option::is_none", with = "date_format::option")]
991 pub start_date_time: Option<DateTime<Utc>>,
992 /// End of the period (UTC). Left out when `None`.
993 #[serde(skip_serializing_if = "Option::is_none", with = "date_format::option")]
994 pub end_date_time: Option<DateTime<Utc>>,
995 /// `1` buy, `2` sell.
996 #[serde(skip_serializing_if = "Option::is_none")]
997 pub side: Option<String>,
998}
999
1000/// One trade of the tape. The docs give `dateTime`, `side` (`1` buy, `2` sell), `price`, `volume` (in
1001/// the currency) and `quantity` (pieces), and NO instrument: it is the one asked for. `ticker` and
1002/// `classCode` are read if the broker sends them (older samples have them).
1003///
1004/// # Examples
1005///
1006/// ```
1007/// # use rust_decimal::Decimal;
1008/// # use rust_decimal_macros::dec;
1009/// use brokapi_bcs_protocol::http::models::LastTradeRecord;
1010///
1011/// let t: LastTradeRecord = serde_json::from_str(
1012/// r#"{"dateTime":"2026-10-19T07:05:00.000Z","side":"1","price":300.1,"volume":60020,"quantity":200}"#,
1013/// )?;
1014/// assert_eq!(t.quantity, dec!(200.0));
1015/// assert!(t.ticker.is_none()); // the documented record has no instrument
1016/// # Ok::<(), serde_json::Error>(())
1017/// ```
1018#[derive(Debug, Clone, Deserialize, Serialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct LastTradeRecord {
1021 /// Ticker, if the broker sends it. The documented record has none: it is the instrument you asked for.
1022 #[serde(default)]
1023 pub ticker: Option<String>,
1024 /// Class code, if the broker sends it. The documented record has none.
1025 #[serde(default)]
1026 pub class_code: Option<String>,
1027 /// Side of the trade as text: `1` buy, `2` sell on REST. Older samples carry `BUY` and `SELL`.
1028 #[serde(default)]
1029 pub side: Option<String>,
1030 /// Trade volume in the instrument currency.
1031 #[serde(default)]
1032 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1033 pub volume: Option<Decimal>,
1034 /// Trade price.
1035 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
1036 pub price: Decimal,
1037 /// Quantity in pieces.
1038 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision::serialize")]
1039 pub quantity: Decimal,
1040 /// Time of the trade (UTC), kept as the text the broker sends.
1041 #[serde(default)]
1042 pub date_time: Option<String>,
1043}
1044
1045/// The anonymous trades of the session: a list of [`LastTradeRecord`]. See [`BcsHttpClient::get_last_trades`](crate::http::client::BcsHttpClient::get_last_trades).
1046pub type LastTradesResponse = Vec<LastTradeRecord>;
1047
1048/// The two shapes the tape comes in: the docs say `{"records": [...]}`, the first samples and the
1049/// fixtures are a bare array. Both are read.
1050#[derive(Debug, Clone, Deserialize)]
1051#[serde(untagged)]
1052pub(crate) enum LastTradesWire {
1053 Records { records: Vec<LastTradeRecord> },
1054 Plain(Vec<LastTradeRecord>),
1055}
1056
1057impl LastTradesWire {
1058 pub(crate) fn into_vec(self) -> LastTradesResponse {
1059 match self {
1060 Self::Records { records } | Self::Plain(records) => records,
1061 }
1062 }
1063}
1064
1065// ─── quotes ───────────────────────────────────────────────────────────────────
1066
1067/// One instrument of a quotes request (`POST /api/v1/quotes`, schema `QuotesRequest.instruments[]`).
1068///
1069/// # Examples
1070///
1071/// ```
1072/// use brokapi_bcs_protocol::http::models::QuoteInstrument;
1073///
1074/// let q = QuoteInstrument::new("SBER", "TQBR");
1075/// assert_eq!(serde_json::to_string(&q)?, r#"{"ticker":"SBER","classCode":"TQBR"}"#);
1076/// # Ok::<(), serde_json::Error>(())
1077/// ```
1078#[derive(Debug, Clone, PartialEq, Serialize)]
1079#[serde(rename_all = "camelCase")]
1080pub struct QuoteInstrument {
1081 /// Ticker.
1082 pub ticker: String,
1083 /// Class code (board).
1084 pub class_code: String,
1085}
1086
1087impl QuoteInstrument {
1088 /// Builds the key of one instrument for a quotes request.
1089 pub fn new(ticker: impl Into<String>, class_code: impl Into<String>) -> Self {
1090 Self {
1091 ticker: ticker.into(),
1092 class_code: class_code.into(),
1093 }
1094 }
1095}
1096
1097/// `POST /api/v1/quotes` body. The docs allow 1 to 100 instruments.
1098///
1099/// # Examples
1100///
1101/// ```
1102/// use brokapi_bcs_protocol::http::models::{QuoteInstrument, QuotesRequest};
1103///
1104/// let req = QuotesRequest { instruments: vec![QuoteInstrument::new("SBER", "TQBR")] };
1105/// assert_eq!(
1106/// serde_json::to_string(&req)?,
1107/// r#"{"instruments":[{"ticker":"SBER","classCode":"TQBR"}]}"#
1108/// );
1109/// # Ok::<(), serde_json::Error>(())
1110/// ```
1111#[derive(Debug, Clone, PartialEq, Serialize)]
1112pub struct QuotesRequest {
1113 /// The instruments to quote. The docs allow 1 to 100.
1114 pub instruments: Vec<QuoteInstrument>,
1115}
1116
1117/// Current quote of one instrument (schema `MarketDataResponse`). Every field except the key may be
1118/// absent: the docs mark none of them required.
1119///
1120/// # Examples
1121///
1122/// ```
1123/// use brokapi_bcs_protocol::http::models::QuoteRecord;
1124///
1125/// let q: QuoteRecord = serde_json::from_str(
1126/// r#"{"ticker":"SBER","classCode":"TQBR","securityTradingStatus":17,"bid":305.86,"offer":305.96,"last":305.97}"#,
1127/// )?;
1128/// assert_eq!(q.security_trading_status, Some(17)); // 17 = trading is open
1129/// assert!(q.theoretical_price.is_none()); // a field the broker left out
1130/// # Ok::<(), serde_json::Error>(())
1131/// ```
1132#[derive(Debug, Clone, PartialEq, Deserialize)]
1133#[serde(rename_all = "camelCase")]
1134pub struct QuoteRecord {
1135 /// Ticker.
1136 #[serde(default)]
1137 pub ticker: Option<String>,
1138 /// Class code (board).
1139 #[serde(default)]
1140 pub class_code: Option<String>,
1141 /// Data update time (UTC), ISO 8601.
1142 #[serde(default)]
1143 pub date_time: Option<String>,
1144 /// 2 halted, 17 open, 18 closed, 100 closing, 101 opening, 102 auction, 103 closing auction,
1145 /// 104 discrete auction (docs, `securityTradingStatus`).
1146 #[serde(default)]
1147 pub security_trading_status: Option<i32>,
1148 /// Price currency.
1149 #[serde(default)]
1150 pub currency: Option<String>,
1151 /// Best buy price.
1152 #[serde(default)]
1153 pub bid: Option<Decimal>,
1154 /// Best sell price.
1155 #[serde(default)]
1156 pub offer: Option<Decimal>,
1157 /// Opening price of the session.
1158 #[serde(default)]
1159 pub open: Option<Decimal>,
1160 /// Close of the previous session.
1161 #[serde(default)]
1162 pub close: Option<Decimal>,
1163 /// Highest price of the day.
1164 #[serde(default)]
1165 pub high: Option<Decimal>,
1166 /// Lowest price of the day.
1167 #[serde(default)]
1168 pub low: Option<Decimal>,
1169 /// Calculated price (options).
1170 #[serde(default)]
1171 pub theoretical_price: Option<Decimal>,
1172 /// Price of the last trade.
1173 #[serde(default)]
1174 pub last: Option<Decimal>,
1175 /// Yield of the buy quote (for bonds).
1176 #[serde(default)]
1177 pub bid_yield: Option<f64>,
1178 /// Yield of the sell quote (for bonds).
1179 #[serde(default)]
1180 pub offer_yield: Option<f64>,
1181 /// Price change over the session, in the price currency.
1182 #[serde(default)]
1183 pub change: Option<Decimal>,
1184 /// Price change over the session, percent.
1185 #[serde(default)]
1186 pub change_rate: Option<f64>,
1187}
1188
1189/// Response of `POST /api/v1/quotes` (schema `QuotesListResponse`).
1190#[derive(Debug, Clone, PartialEq, Deserialize)]
1191pub struct QuotesResponse {
1192 /// One quote per instrument that the broker knows.
1193 #[serde(default)]
1194 pub records: Vec<QuoteRecord>,
1195}
1196
1197// ─── non-trade operations ─────────────────────────────────────────────────────
1198
1199/// `POST /api/v1/operations/search` body (schema `OperationRequest`), all filters optional.
1200///
1201/// `operation_types`: PayOut, PayIn, PayTransfer, Dividend, DividendDebt, BondPayingOff, Commission,
1202/// IncomeTax, SecurityOut, SecurityIn, BondPartRepayment, BondFullRepayment,
1203/// StructuralProductRepayment, InvestmentShareRepayment, PayOutFx, PayOutDfa, PayOutDu, PayInFx,
1204/// PayInDfa, VarMargin. `statuses`: Approved, InProgress, Rejected. Kept as strings so a value the
1205/// broker adds later does not break parsing.
1206///
1207/// # Examples
1208///
1209/// ```
1210/// use brokapi_bcs_protocol::http::models::OperationHistoryRequest;
1211///
1212/// let req = OperationHistoryRequest {
1213/// operation_types: Some(vec!["Dividend".into(), "BondPayingOff".into()]),
1214/// ..Default::default()
1215/// };
1216/// assert_eq!(
1217/// serde_json::to_value(&req)?,
1218/// serde_json::json!({ "operationTypes": ["Dividend", "BondPayingOff"] })
1219/// );
1220/// # Ok::<(), serde_json::Error>(())
1221/// ```
1222#[derive(Debug, Clone, Default, PartialEq, Serialize)]
1223#[serde(rename_all = "camelCase")]
1224pub struct OperationHistoryRequest {
1225 /// Operation types to include, in the broker spelling (see the list above). All types if `None`.
1226 #[serde(skip_serializing_if = "Option::is_none")]
1227 pub operation_types: Option<Vec<String>>,
1228 /// Statuses to include: `Approved`, `InProgress`, `Rejected`. All if `None`.
1229 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub statuses: Option<Vec<String>>,
1231 /// Start of the period, ISO 8601 date-time.
1232 #[serde(skip_serializing_if = "Option::is_none")]
1233 pub start_date_time: Option<String>,
1234 /// End of the period, ISO 8601 date-time.
1235 #[serde(skip_serializing_if = "Option::is_none")]
1236 pub end_date_time: Option<String>,
1237 /// Only operations on these ISINs.
1238 #[serde(skip_serializing_if = "Option::is_none")]
1239 pub isins: Option<Vec<String>>,
1240 /// Only operations on these tickers.
1241 #[serde(skip_serializing_if = "Option::is_none")]
1242 pub tickers: Option<Vec<String>>,
1243 /// Only operations in these currencies.
1244 #[serde(skip_serializing_if = "Option::is_none")]
1245 pub currencies: Option<Vec<String>>,
1246}
1247
1248/// One non-trade operation (schema `Operation`): deposits, withdrawals, dividends, coupons,
1249/// commissions, taxes, securities in and out.
1250///
1251/// # Examples
1252///
1253/// ```
1254/// use brokapi_bcs_protocol::http::models::OperationRecord;
1255///
1256/// let op: OperationRecord = serde_json::from_str(
1257/// r#"{"id":"op-1","type":"PayIn","status":"Approved","sum":1000.0,"currency":"RUB","balanceChange":"Positive"}"#,
1258/// )?;
1259/// assert_eq!(op.operation_type.as_deref(), Some("PayIn"));
1260/// # Ok::<(), serde_json::Error>(())
1261/// ```
1262#[derive(Debug, Clone, PartialEq, Deserialize)]
1263#[serde(rename_all = "camelCase")]
1264pub struct OperationRecord {
1265 /// Id of the operation.
1266 #[serde(default)]
1267 pub id: Option<String>,
1268 /// Date of the operation, kept as text.
1269 #[serde(default)]
1270 pub date: Option<String>,
1271 /// IIA (tax-advantaged account) flag.
1272 #[serde(default)]
1273 pub is_iia: Option<bool>,
1274 /// Ticker of the issuer, for securities operations.
1275 #[serde(default)]
1276 pub ticker: Option<String>,
1277 /// Class code of the issuer, for securities operations.
1278 #[serde(default)]
1279 pub class_code: Option<String>,
1280 /// Operation type, see [`OperationHistoryRequest`] for the values.
1281 #[serde(default, rename = "type")]
1282 pub operation_type: Option<String>,
1283 /// Approved, InProgress or Rejected.
1284 #[serde(default)]
1285 pub status: Option<String>,
1286 /// Amount of the operation.
1287 #[serde(default)]
1288 pub sum: Option<Decimal>,
1289 /// Currency of the operation.
1290 #[serde(default)]
1291 pub currency: Option<String>,
1292 /// The whole remaining balance is withdrawn.
1293 #[serde(default)]
1294 pub is_rests_pay_out: Option<bool>,
1295 /// ISIN of the instrument.
1296 #[serde(default)]
1297 pub isin: Option<String>,
1298 /// Name of the instrument (the broker docs say "name of the financial instrument").
1299 #[serde(default)]
1300 pub issuer_name: Option<String>,
1301 /// How the account was topped up.
1302 #[serde(default)]
1303 pub replenishment_type: Option<String>,
1304 /// Expected time to execute the operation.
1305 #[serde(default)]
1306 pub execution_period: Option<String>,
1307 /// Neutral, Negative or Positive.
1308 #[serde(default)]
1309 pub balance_change: Option<String>,
1310 /// Why the operation was rejected, if it was.
1311 #[serde(default)]
1312 pub rejection_reason: Option<String>,
1313}
1314
1315/// Response of `POST /api/v1/operations/search` (schema `OperationListResponse`). Unlike the other
1316/// paginated lists, the docs give only `records` and `pageSize` (no total).
1317///
1318/// # Examples
1319///
1320/// ```
1321/// use brokapi_bcs_protocol::http::models::OperationHistoryResponse;
1322///
1323/// let page: OperationHistoryResponse = serde_json::from_str(r#"{"records":[],"pageSize":50}"#)?;
1324/// assert_eq!(page.page_size, Some(50));
1325/// # Ok::<(), serde_json::Error>(())
1326/// ```
1327#[derive(Debug, Clone, PartialEq, Deserialize)]
1328#[serde(rename_all = "camelCase")]
1329pub struct OperationHistoryResponse {
1330 /// The operations of the page.
1331 #[serde(default)]
1332 pub records: Vec<OperationRecord>,
1333 /// Records per page, as the broker reports it. There is no total count: read pages until one is shorter than the page size.
1334 #[serde(default)]
1335 pub page_size: Option<u32>,
1336}
1337
1338// ─── orders ───────────────────────────────────────────────────────────────────
1339
1340/// Type of an order for placement (`orderType` of the create and edit requests). The wire form is a digit as text.
1341///
1342/// The connector sends market and limit orders, and conditional orders for stop orders. The other types
1343/// are in the broker docs and need the optional parameters of [`OrderExtras`]. How a combination
1344/// behaves is described in the docs of the broker and is not verified against the live API.
1345///
1346/// # Examples
1347///
1348/// ```
1349/// use brokapi_bcs_protocol::http::models::OrderType;
1350///
1351/// assert_eq!(serde_json::to_string(&OrderType::Limit)?, "\"2\"");
1352/// assert_eq!(serde_json::to_string(&OrderType::Conditional)?, "\"5\"");
1353/// # Ok::<(), serde_json::Error>(())
1354/// ```
1355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1356pub enum OrderType {
1357 /// Market order (`"1"`).
1358 #[serde(rename = "1")]
1359 Market,
1360 /// Limit order (`"2"`).
1361 #[serde(rename = "2")]
1362 Limit,
1363 /// Algorithmic "good till date" order (`timeInForce = 3`).
1364 #[serde(rename = "3")]
1365 Algorithmic,
1366 /// Iceberg: `visible` is the shown quantity.
1367 #[serde(rename = "4")]
1368 Iceberg,
1369 /// Conditional: stop-loss and/or take-profit (`stopPrice`, `takePrice`, `execution*Type`, `spread`,
1370 /// `offset`). Added to the docs after the first live runs. The connector uses it for stop orders.
1371 #[serde(rename = "5")]
1372 Conditional,
1373}
1374
1375/// Body of [`BcsHttpClient::search_orders`](crate::http::client::BcsHttpClient::search_orders).
1376///
1377/// The filters use the codes of the **order list**, which differ from the codes of order events. Status:
1378/// `1` cancelled, `2` executed, `3` active. Side: `1` buy, `2` sell. Types: `1` market, `2` limit,
1379/// `3` iceberg, `4` stop-limit, `5` take-profit (makes a limit order), `6` stop-loss, `7` take-profit and
1380/// stop-loss, `10` limit order for 30 days, `11` take-profit. Filters left as `None` are not sent.
1381///
1382/// # Examples
1383///
1384/// ```
1385/// use brokapi_bcs_protocol::http::models::OrderSearchRequest;
1386/// use chrono::{TimeZone, Utc};
1387///
1388/// let req = OrderSearchRequest {
1389/// start_date_time: Utc.with_ymd_and_hms(2026, 10, 19, 0, 0, 0).unwrap(),
1390/// end_date_time: Utc.with_ymd_and_hms(2026, 10, 20, 0, 0, 0).unwrap(),
1391/// side: Some(1),
1392/// order_status: Some(vec![3]),
1393/// order_types: None,
1394/// tickers: Some(vec!["SBER".into()]),
1395/// class_codes: None,
1396/// };
1397/// let json = serde_json::to_value(&req)?;
1398/// assert_eq!(json["orderStatus"], serde_json::json!([3]));
1399/// assert!(json.get("orderTypes").is_none());
1400/// # Ok::<(), serde_json::Error>(())
1401/// ```
1402#[derive(Debug, Clone, Serialize)]
1403#[serde(rename_all = "camelCase")]
1404pub struct OrderSearchRequest {
1405 /// Start of the period (UTC).
1406 #[serde(with = "date_format::moex")]
1407 pub start_date_time: DateTime<Utc>,
1408 /// End of the period (UTC).
1409 #[serde(with = "date_format::moex")]
1410 pub end_date_time: DateTime<Utc>,
1411 /// `1` buy or `2` sell. Both if `None`.
1412 #[serde(skip_serializing_if = "Option::is_none")]
1413 pub side: Option<i32>,
1414 /// Statuses of the order list: `1` cancelled, `2` executed, `3` active. All if `None`.
1415 #[serde(skip_serializing_if = "Option::is_none")]
1416 pub order_status: Option<Vec<i32>>,
1417 /// Types of the order list (see above). All if `None`.
1418 #[serde(skip_serializing_if = "Option::is_none")]
1419 pub order_types: Option<Vec<i32>>,
1420 /// Only these tickers.
1421 #[serde(skip_serializing_if = "Option::is_none")]
1422 pub tickers: Option<Vec<String>>,
1423 /// Only these class codes (boards).
1424 #[serde(skip_serializing_if = "Option::is_none")]
1425 pub class_codes: Option<Vec<String>>,
1426}
1427
1428/// One page of the order list, with the totals.
1429#[derive(Debug, Clone, Deserialize)]
1430#[serde(rename_all = "camelCase")]
1431pub struct OrderSearchResponse {
1432 /// The orders of the page.
1433 pub records: Vec<OrderRecord>,
1434 /// Number of orders for the whole filter.
1435 pub total_records: i64,
1436 /// Number of pages for the whole filter.
1437 pub total_pages: i32,
1438}
1439
1440/// One order in the order list (`POST .../orders/search`).
1441///
1442/// This record is **not** the same as the order state of [`OrderExecutionData`]. It uses numbers for side,
1443/// type and status, with the meaning of the **list**: status `1` cancelled, `2` executed, `3` active.
1444/// It carries the broker's order id and no `clientOrderId`. Quantities come in pieces and in lots.
1445///
1446/// # Examples
1447///
1448/// ```
1449/// use brokapi_bcs_protocol::http::models::OrderRecord;
1450///
1451/// let r: OrderRecord = serde_json::from_str(
1452/// r#"{"orderId":"20261019-TQBR-79000001","orderNum":79000001,"ticker":"SBER","classCode":"TQBR",
1453/// "side":1,"orderType":2,"orderStatus":3,"price":300.0,"orderQuantity":1,"remainedQuantity":1,
1454/// "orderDateTime":"2026-10-19T07:05:00.000Z"}"#,
1455/// )?;
1456/// assert_eq!(r.order_status, Some(3)); // 3 = active in the list
1457/// assert!(r.order_date_time.is_some());
1458/// # Ok::<(), serde_json::Error>(())
1459/// ```
1460#[derive(Debug, Clone, Deserialize)]
1461#[serde(rename_all = "camelCase")]
1462pub struct OrderRecord {
1463 /// Id of the order at the exchange, in the form `YYYYMMDD-ClassCode-OrderNumber`. The only field the code requires.
1464 pub order_id: String,
1465 /// Order number.
1466 #[serde(default)]
1467 pub order_num: Option<i64>,
1468 /// Client code.
1469 #[serde(default)]
1470 pub client_code: Option<String>,
1471 /// Executed volume in the currency.
1472 #[serde(default)]
1473 pub executed_value: Option<Decimal>,
1474 /// Trading date, kept as text.
1475 #[serde(default)]
1476 pub trade_date: Option<String>,
1477 /// Time the order was placed (UTC). `None` if absent or the epoch.
1478 #[serde(default, with = "date_format::option")]
1479 pub order_date_time: Option<DateTime<Utc>>,
1480 /// Time of the last update (UTC). `None` if absent or the epoch.
1481 #[serde(default, with = "date_format::option")]
1482 pub update_date_time: Option<DateTime<Utc>>,
1483 /// Ticker.
1484 #[serde(skip_serializing_if = "Option::is_none")]
1485 pub ticker: Option<String>,
1486 /// Class code (board).
1487 #[serde(skip_serializing_if = "Option::is_none")]
1488 pub class_code: Option<String>,
1489 /// `1` buy, `2` sell.
1490 #[serde(skip_serializing_if = "Option::is_none")]
1491 pub side: Option<i32>,
1492 /// Type of the order, the codes of the order list (see [`OrderSearchRequest`]).
1493 #[serde(skip_serializing_if = "Option::is_none")]
1494 pub order_type: Option<i32>,
1495 /// Status of the order, the codes of the order list: `1` cancelled, `2` executed, `3` active. These are not the codes of order events.
1496 #[serde(skip_serializing_if = "Option::is_none")]
1497 pub order_status: Option<i32>,
1498 /// Quantity of the order, in pieces.
1499 #[serde(skip_serializing_if = "Option::is_none")]
1500 pub order_quantity: Option<Decimal>,
1501 /// Executed quantity, in pieces.
1502 #[serde(skip_serializing_if = "Option::is_none")]
1503 pub executed_quantity: Option<Decimal>,
1504 /// Price of the order.
1505 #[serde(skip_serializing_if = "Option::is_none")]
1506 pub price: Option<Decimal>,
1507 /// Average execution price.
1508 #[serde(skip_serializing_if = "Option::is_none")]
1509 pub average_price: Option<Decimal>,
1510 // Fields of the current docs (`POST /orders/search`). The list does not carry our `clientOrderId`.
1511 /// Remaining quantity, in pieces.
1512 #[serde(default)]
1513 pub remained_quantity: Option<Decimal>,
1514 /// Quantity of the order, in lots.
1515 #[serde(default)]
1516 pub order_quantity_lots: Option<Decimal>,
1517 /// Remaining quantity, in lots.
1518 #[serde(default)]
1519 pub remained_quantity_lots: Option<Decimal>,
1520 /// Executed quantity, in lots.
1521 #[serde(default)]
1522 pub executed_quantity_lots: Option<Decimal>,
1523 /// Settlement currency.
1524 #[serde(default)]
1525 pub settlement_currency: Option<String>,
1526 /// Why the order was rejected, if it was.
1527 #[serde(default)]
1528 pub reject_reason: Option<String>,
1529 /// Conditional orders: take-profit and stop-loss trigger prices.
1530 #[serde(default)]
1531 pub take_price: Option<Decimal>,
1532 /// Stop-loss trigger price of a conditional order.
1533 #[serde(default)]
1534 pub stop_price: Option<Decimal>,
1535 /// Number of the linked order (the other leg of a TP+SL pair, or the order a stop produced).
1536 #[serde(default)]
1537 pub linked_order: Option<String>,
1538 /// Number of the stop order.
1539 #[serde(default)]
1540 pub stop_order: Option<String>,
1541 /// `1` market, `2` limit.
1542 #[serde(default)]
1543 pub market_take_profit: Option<i32>,
1544 /// Stop-loss execution: `1` market order, `2` limit order.
1545 #[serde(default)]
1546 pub market_stop_loss: Option<i32>,
1547 /// Stop price by position.
1548 #[serde(default)]
1549 pub position_price_stop: Option<Decimal>,
1550 /// Limit price by position.
1551 #[serde(default)]
1552 pub position_price_limit: Option<Decimal>,
1553}
1554
1555/// Optional order parameters of the current docs (`POST /api/v1/orders`, `/orders/edit`, and their
1556/// `/sync` twins): time in force, stop-loss and take-profit, iceberg, trailing spread and offset.
1557/// Everything is optional and omitted when `None`. What a combination means for the broker is in the
1558/// docs. The connector fills them for its stop orders (conditional orders).
1559///
1560/// # Examples
1561///
1562/// ```
1563/// use brokapi_bcs_protocol::http::models::OrderExtras;
1564///
1565/// // Good Till Date: valid until the given day.
1566/// let extras = OrderExtras {
1567/// time_in_force: Some("3".into()),
1568/// expiry_date: Some("20261120".into()),
1569/// ..Default::default()
1570/// };
1571/// let json = serde_json::to_value(&extras)?;
1572/// assert_eq!(json["timeInForce"], "3");
1573/// assert!(json.get("stopPrice").is_none()); // unset fields are not sent
1574/// # Ok::<(), serde_json::Error>(())
1575/// ```
1576#[derive(Debug, Clone, Default, PartialEq, Serialize)]
1577#[serde(rename_all = "camelCase")]
1578pub struct OrderExtras {
1579 /// `1` Day (default), `2` Fill Or Kill, `3` Good Till Date (algorithmic orders only).
1580 #[serde(skip_serializing_if = "Option::is_none")]
1581 pub time_in_force: Option<String>,
1582 /// `YYYYMMDD`, required with `time_in_force = "3"`, at most 30 days ahead.
1583 #[serde(skip_serializing_if = "Option::is_none")]
1584 pub expiry_date: Option<String>,
1585 /// Stop price of a conditional order. Required for scenarios with a stop-loss.
1586 #[serde(skip_serializing_if = "Option::is_none")]
1587 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1588 pub stop_price: Option<Decimal>,
1589 /// Take-profit price of a conditional order. Required for scenarios with a take-profit.
1590 #[serde(skip_serializing_if = "Option::is_none")]
1591 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1592 pub take_price: Option<Decimal>,
1593 /// Shown quantity of an iceberg order, below `order_quantity`.
1594 #[serde(skip_serializing_if = "Option::is_none")]
1595 pub visible: Option<i64>,
1596 /// `1` market, `2` limit.
1597 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub execution_take_profit_type: Option<i32>,
1599 /// `1` market, `2` limit.
1600 #[serde(skip_serializing_if = "Option::is_none")]
1601 pub execution_stop_loss_type: Option<i32>,
1602 /// Protective spread of a conditional order.
1603 #[serde(skip_serializing_if = "Option::is_none")]
1604 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1605 pub spread: Option<Decimal>,
1606 /// `0` price currency, `1` percent.
1607 #[serde(skip_serializing_if = "Option::is_none")]
1608 pub spread_type: Option<i32>,
1609 /// Offset of a conditional order.
1610 #[serde(skip_serializing_if = "Option::is_none")]
1611 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1612 pub offset: Option<Decimal>,
1613 /// `0` price currency, `1` percent.
1614 #[serde(skip_serializing_if = "Option::is_none")]
1615 pub offset_type: Option<i32>,
1616}
1617
1618/// Body of an order placement: [`BcsHttpClient::create_order`](crate::http::client::BcsHttpClient::create_order)
1619/// and [`BcsHttpClient::create_order_sync`](crate::http::client::BcsHttpClient::create_order_sync).
1620///
1621/// Build it with [`CreateOrderRequest::market`] or [`CreateOrderRequest::limit`]. The quantity is in
1622/// **pieces**, not lots, and the price is per piece. A price you send must be a whole number of price steps
1623/// ([`crate::utils::precision`]) and inside the exchange band, or the exchange refuses the order.
1624///
1625/// The fields of [`OrderExtras`] are flattened into the body, all of them optional.
1626///
1627/// # Examples
1628///
1629/// ```
1630/// # use rust_decimal::Decimal;
1631/// # use rust_decimal_macros::dec;
1632/// use brokapi_bcs_protocol::http::models::CreateOrderRequest;
1633/// use brokapi_bcs_protocol::models::enums::OrderSide;
1634/// use uuid::Uuid;
1635///
1636/// let req = CreateOrderRequest::limit(Uuid::nil(), "SBER", "TQBR", OrderSide::Buy, 10, dec!(300.5));
1637/// assert_eq!(
1638/// serde_json::to_value(&req)?,
1639/// serde_json::json!({
1640/// "clientOrderId": "00000000-0000-0000-0000-000000000000",
1641/// "side": "1",
1642/// "orderType": "2",
1643/// "orderQuantity": 10,
1644/// "ticker": "SBER",
1645/// "classCode": "TQBR",
1646/// "price": 300.5
1647/// })
1648/// );
1649/// # Ok::<(), serde_json::Error>(())
1650/// ```
1651#[derive(Debug, Clone, Serialize)]
1652#[serde(rename_all = "camelCase")]
1653pub struct CreateOrderRequest {
1654 /// Your id of the order. Generate a new UUID per order and keep it: it is how you find the order later. The broker docs say it is used to track the order and to prevent duplicates.
1655 pub client_order_id: Uuid,
1656 /// Buy or sell.
1657 pub side: OrderSide,
1658 /// Type of the order.
1659 pub order_type: OrderType,
1660 /// Quantity in pieces (not lots), at least 1.
1661 pub order_quantity: i64,
1662 /// Ticker.
1663 pub ticker: String,
1664 /// Class code (board), for example `TQBR`.
1665 pub class_code: String,
1666 /// Price per piece, above 0, up to 8 decimals. Left out for a market order.
1667 #[serde(skip_serializing_if = "Option::is_none")]
1668 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1669 pub price: Option<Decimal>,
1670 /// Optional parameters (time in force, stops, iceberg, spread, offset), flattened into the body.
1671 #[serde(flatten)]
1672 pub extras: OrderExtras,
1673}
1674
1675impl CreateOrderRequest {
1676 /// Builds a market order. The `client_order_id` is yours: generate a new UUID per order and keep it.
1677 ///
1678 /// The quantity is in pieces. A market order has no price. The order is not sent by this call, pass
1679 /// the result to [`BcsHttpClient::create_order`](crate::http::client::BcsHttpClient::create_order).
1680 ///
1681 /// # Examples
1682 ///
1683 /// ```
1684 /// use brokapi_bcs_protocol::http::models::CreateOrderRequest;
1685 /// use brokapi_bcs_protocol::models::enums::OrderSide;
1686 /// use uuid::Uuid;
1687 ///
1688 /// let req = CreateOrderRequest::market(Uuid::new_v4(), "SBER", "TQBR", OrderSide::Sell, 3);
1689 /// assert!(req.price.is_none());
1690 /// assert_eq!(serde_json::to_value(&req)?["orderType"], "1");
1691 /// # Ok::<(), serde_json::Error>(())
1692 /// ```
1693 pub fn market(
1694 client_order_id: Uuid,
1695 ticker: impl Into<String>,
1696 class_code: impl Into<String>,
1697 side: OrderSide,
1698 quantity: i64,
1699 ) -> Self {
1700 Self {
1701 client_order_id,
1702 side,
1703 order_type: OrderType::Market,
1704 order_quantity: quantity,
1705 ticker: ticker.into(),
1706 class_code: class_code.into(),
1707 price: None,
1708 extras: OrderExtras::default(),
1709 }
1710 }
1711
1712 /// Builds a limit order. The `client_order_id` is yours: generate a new UUID per order and keep it.
1713 ///
1714 /// The quantity is in pieces and the price is per piece. The price must be a whole number of price
1715 /// steps and inside the exchange band, or the order is refused. See [`crate::utils::precision`] to
1716 /// put a price on the step grid. The order is not sent by this call, pass the result to
1717 /// [`BcsHttpClient::create_order`](crate::http::client::BcsHttpClient::create_order).
1718 ///
1719 /// # Examples
1720 ///
1721 /// ```
1722 /// use brokapi_bcs_protocol::http::models::CreateOrderRequest;
1723 /// use brokapi_bcs_protocol::models::enums::OrderSide;
1724 /// use brokapi_bcs_protocol::utils::precision::precise_down;
1725 /// use rust_decimal::Decimal;
1726 /// use uuid::Uuid;
1727 ///
1728 /// // The price comes from a calculation: put it on the 0.05 step grid first.
1729 /// let price = precise_down(Decimal::new(10103, 2), Decimal::new(5, 2))?;
1730 /// let req = CreateOrderRequest::limit(Uuid::new_v4(), "SBER", "TQBR", OrderSide::Buy, 2, price);
1731 /// assert_eq!(req.price, Some(Decimal::new(10100, 2)));
1732 /// # Ok::<(), brokapi_bcs_protocol::utils::precision::PrecisionError>(())
1733 /// ```
1734 pub fn limit(
1735 client_order_id: Uuid,
1736 ticker: impl Into<String>,
1737 class_code: impl Into<String>,
1738 side: OrderSide,
1739 quantity: i64,
1740 price: Decimal,
1741 ) -> Self {
1742 Self {
1743 client_order_id,
1744 side,
1745 order_type: OrderType::Limit,
1746 order_quantity: quantity,
1747 ticker: ticker.into(),
1748 class_code: class_code.into(),
1749 price: Some(price),
1750 extras: OrderExtras::default(),
1751 }
1752 }
1753}
1754
1755/// Body of the **deprecated** cancel, [`BcsHttpClient::cancel_order`](crate::http::client::BcsHttpClient::cancel_order).
1756///
1757/// Use [`CancelByIdRequest`] with [`BcsHttpClient::cancel_order_by_id`](crate::http::client::BcsHttpClient::cancel_order_by_id)
1758/// instead.
1759#[derive(Debug, Clone, Serialize)]
1760#[serde(rename_all = "camelCase")]
1761pub struct CancelOrderRequest {
1762 /// Id of this cancel request (a new UUID).
1763 pub client_order_id: Uuid,
1764}
1765
1766/// Body of the **deprecated** change, [`BcsHttpClient::modify_order`](crate::http::client::BcsHttpClient::modify_order).
1767///
1768/// Use [`EditOrderRequest`] with [`BcsHttpClient::edit_order`](crate::http::client::BcsHttpClient::edit_order) instead.
1769#[derive(Debug, Clone, Serialize)]
1770#[serde(rename_all = "camelCase")]
1771pub struct ModifyOrderRequest {
1772 /// Id of this change request (a new UUID).
1773 pub client_order_id: Uuid,
1774 /// New quantity in pieces.
1775 pub order_quantity: i64,
1776 /// New price per piece. Left out when `None`.
1777 #[serde(skip_serializing_if = "Option::is_none")]
1778 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1779 pub price: Option<Decimal>,
1780}
1781
1782/// `POST /api/v1/orders/cancel` and `/orders/cancel/sync` body: the order is named in the body, not
1783/// in the path (the `/orders/{id}/cancel` of `CancelOrderRequest` is the deprecated form).
1784///
1785/// # Examples
1786///
1787/// ```
1788/// use brokapi_bcs_protocol::http::models::CancelByIdRequest;
1789/// use uuid::Uuid;
1790///
1791/// let original = Uuid::nil();
1792/// let req = CancelByIdRequest::by_client_order_id(original, Uuid::nil());
1793/// let json = serde_json::to_value(&req)?;
1794/// assert_eq!(json["orderIdType"], "1");
1795/// assert_eq!(json["orderId"], "00000000-0000-0000-0000-000000000000");
1796/// # Ok::<(), serde_json::Error>(())
1797/// ```
1798#[derive(Debug, Clone, PartialEq, Serialize)]
1799#[serde(rename_all = "camelCase")]
1800pub struct CancelByIdRequest {
1801 /// `1` = `order_id` is the `clientOrderId` of the original order (default), `2` = exchange order id.
1802 #[serde(skip_serializing_if = "Option::is_none")]
1803 pub order_id_type: Option<String>,
1804 /// The id of the order to cancel, of the kind named by `order_id_type`. For kind `1` a UUID text, for kind `2` the exchange id in the form `YYMMDD-ClassCode-OrderNumber`.
1805 pub order_id: String,
1806 /// Id of this request, a new UUID.
1807 pub client_order_id: Uuid,
1808}
1809
1810impl CancelByIdRequest {
1811 /// Builds a cancel for the order whose `clientOrderId` is `original`.
1812 ///
1813 /// `request_id` is the id of the cancel request itself, a new UUID. The call sets the id kind to
1814 /// `1` (the `clientOrderId` of the original). To cancel by the exchange id build the struct
1815 /// yourself with `order_id_type = Some("2")`.
1816 pub fn by_client_order_id(original: Uuid, request_id: Uuid) -> Self {
1817 Self {
1818 order_id_type: Some("1".into()),
1819 order_id: original.hyphenated().to_string(),
1820 client_order_id: request_id,
1821 }
1822 }
1823}
1824
1825/// `POST /api/v1/orders/edit` and `/orders/edit/sync` body. The edit is a cancel of the original and
1826/// a new order (docs), so `client_order_id` is the id of the new one.
1827///
1828/// # Examples
1829///
1830/// ```
1831/// # use rust_decimal::Decimal;
1832/// # use rust_decimal_macros::dec;
1833/// use brokapi_bcs_protocol::http::models::{EditOrderRequest, OrderExtras};
1834/// use uuid::Uuid;
1835///
1836/// let req = EditOrderRequest {
1837/// order_id_type: None,
1838/// order_id: Uuid::nil().to_string(),
1839/// client_order_id: Uuid::nil(),
1840/// order_type: None,
1841/// price: Some(dec!(301.0)),
1842/// order_quantity: 5,
1843/// extras: OrderExtras::default(),
1844/// };
1845/// let json = serde_json::to_value(&req)?;
1846/// assert_eq!(json["price"], 301.0);
1847/// assert!(json.get("orderIdType").is_none()); // the broker default (1) applies
1848/// # Ok::<(), serde_json::Error>(())
1849/// ```
1850#[derive(Debug, Clone, PartialEq, Serialize)]
1851#[serde(rename_all = "camelCase")]
1852pub struct EditOrderRequest {
1853 /// `1` (default) if `order_id` is the `clientOrderId` of the original order, `2` if it is the exchange order id.
1854 #[serde(skip_serializing_if = "Option::is_none")]
1855 pub order_id_type: Option<String>,
1856 /// The id of the order to change, of the kind named by `order_id_type`.
1857 pub order_id: String,
1858 /// The id of the **new** order that replaces the original. A new UUID.
1859 pub client_order_id: Uuid,
1860 /// Type of the new order. Left out when `None`.
1861 #[serde(skip_serializing_if = "Option::is_none")]
1862 pub order_type: Option<OrderType>,
1863 /// Price of the new order per piece. Left out when `None`.
1864 #[serde(skip_serializing_if = "Option::is_none")]
1865 #[serde(serialize_with = "rust_decimal::serde::arbitrary_precision_option::serialize")]
1866 pub price: Option<Decimal>,
1867 /// Quantity of the new order, in pieces.
1868 pub order_quantity: i64,
1869 /// Optional parameters of the new order, flattened into the body.
1870 #[serde(flatten)]
1871 pub extras: OrderExtras,
1872}
1873
1874/// Answer of the synchronous methods and of `GET /api/v1/orders`: the order's current state. Unlike
1875/// the deprecated status call, `data` is a list.
1876///
1877/// # Examples
1878///
1879/// ```
1880/// use brokapi_bcs_protocol::http::models::OrderStateListResponse;
1881///
1882/// let state: OrderStateListResponse = serde_json::from_str(
1883/// r#"{"clientOrderId":"r-1","originalClientOrderId":"o-1","data":[{"orderStatus":"0"}]}"#,
1884/// )?;
1885/// assert_eq!(state.original_client_order_id.as_deref(), Some("o-1"));
1886/// assert_eq!(state.data.len(), 1);
1887/// # Ok::<(), serde_json::Error>(())
1888/// ```
1889#[derive(Debug, Clone, Deserialize)]
1890#[serde(rename_all = "camelCase")]
1891pub struct OrderStateListResponse {
1892 /// Id of this request (not of the order).
1893 pub client_order_id: String,
1894 /// The `clientOrderId` you gave when you placed the order. Same as [`OrderStatusResponse::original_client_order_id`].
1895 #[serde(default)]
1896 pub original_client_order_id: Option<String>,
1897 /// The reports of the order. Empty if the broker sent none.
1898 #[serde(default)]
1899 pub data: Vec<OrderExecutionData>,
1900}
1901
1902/// The acknowledgement of an asynchronous order command (place, cancel, edit).
1903///
1904/// It means the broker **took the request**. It does not say the order is working or filled. The state
1905/// comes on the order events channel or from
1906/// [`BcsHttpClient::get_order`](crate::http::client::BcsHttpClient::get_order).
1907///
1908/// # Examples
1909///
1910/// ```
1911/// use brokapi_bcs_protocol::http::models::OrderOperationResponse;
1912///
1913/// let ack: OrderOperationResponse = serde_json::from_str(
1914/// r#"{"clientOrderId":"550e8400-e29b-41d4-a716-446655440000","status":"OK"}"#,
1915/// )?;
1916/// assert_eq!(ack.status, "OK");
1917/// # Ok::<(), serde_json::Error>(())
1918/// ```
1919#[derive(Debug, Clone, Deserialize)]
1920#[serde(rename_all = "camelCase")]
1921pub struct OrderOperationResponse {
1922 /// The id you sent: the id of the order for a placement, of the request for a cancel.
1923 pub client_order_id: Uuid,
1924 /// Completion status text of the broker. The docs do not list its values.
1925 pub status: String,
1926}
1927
1928/// The state of an order from the **deprecated** status method,
1929/// [`BcsHttpClient::get_order_status`](crate::http::client::BcsHttpClient::get_order_status).
1930///
1931/// Unlike [`OrderStateListResponse`], `data` is a single object here.
1932#[derive(Debug, Clone, Deserialize)]
1933#[serde(rename_all = "camelCase")]
1934pub struct OrderStatusResponse {
1935 /// Id of the request.
1936 pub client_order_id: String,
1937 /// The `clientOrderId` you gave when you placed the order.
1938 pub original_client_order_id: String,
1939 /// The report of the order.
1940 #[serde(skip_serializing_if = "Option::is_none")]
1941 pub data: Option<OrderExecutionData>,
1942}
1943
1944/// One report about an order: its state, quantities and the last fill.
1945///
1946/// This is the REST form. The WebSocket form is [`crate::ws::models::OrderExecutionData`], which has typed
1947/// enums instead of text. Numbers of quantity are in **pieces**. Status and type codes are text digits:
1948/// status `0` new, `1` partly filled, `2` filled, `4` cancelled, `5` replaced, `6` cancel pending,
1949/// `8` rejected, `9` replace pending, `10` waiting for confirmation. Side `1` buy, `2` sell. Order type
1950/// `1` market, `2` limit, `3` algorithmic, `4` iceberg, `5` conditional.
1951///
1952/// # Examples
1953///
1954/// ```
1955/// # use rust_decimal::Decimal;
1956/// # use rust_decimal_macros::dec;
1957/// use brokapi_bcs_protocol::http::models::OrderStateListResponse;
1958///
1959/// let state: OrderStateListResponse = serde_json::from_str(
1960/// r#"{"clientOrderId":"r-1","originalClientOrderId":"o-1","data":[
1961/// {"orderStatus":"1","executionType":"1","orderQuantity":10,"executedQuantity":4,
1962/// "remainedQuantity":6,"ticker":"SBER","classCode":"TQBR","side":"1","orderType":"2","price":300.0}]}"#,
1963/// )?;
1964/// let report = &state.data[0];
1965/// assert_eq!(report.order_status.as_deref(), Some("1")); // partly filled
1966/// assert_eq!(report.remained_quantity, Some(dec!(6.0)));
1967/// # Ok::<(), serde_json::Error>(())
1968/// ```
1969#[derive(Debug, Clone, Default, Deserialize)]
1970#[serde(rename_all = "camelCase")]
1971pub struct OrderExecutionData {
1972 /// Type of the message, as text.
1973 #[serde(skip_serializing_if = "Option::is_none")]
1974 pub message_type: Option<String>,
1975 /// State of the order, a digit as text (see the struct docs).
1976 #[serde(skip_serializing_if = "Option::is_none")]
1977 pub order_status: Option<String>,
1978 /// Type of the report, a digit as text: `0` new, `1` partial fill, `2` fill, `4` cancelled, `5` replaced, `6` cancel pending, `8` rejected, `9` suspended, `10` waiting for confirmation, `11` trade, `12` order status, `13` corrected.
1979 #[serde(skip_serializing_if = "Option::is_none")]
1980 pub execution_type: Option<String>,
1981 /// Quantity of the order, in pieces.
1982 #[serde(skip_serializing_if = "Option::is_none")]
1983 pub order_quantity: Option<Decimal>,
1984 /// Executed quantity, in pieces.
1985 #[serde(skip_serializing_if = "Option::is_none")]
1986 pub executed_quantity: Option<Decimal>,
1987 /// Quantity of the latest fill, in pieces.
1988 #[serde(skip_serializing_if = "Option::is_none")]
1989 pub last_quantity: Option<Decimal>,
1990 /// Remaining quantity, in pieces.
1991 #[serde(skip_serializing_if = "Option::is_none")]
1992 pub remained_quantity: Option<Decimal>,
1993 /// Ticker.
1994 #[serde(skip_serializing_if = "Option::is_none")]
1995 pub ticker: Option<String>,
1996 /// Class code (board).
1997 #[serde(skip_serializing_if = "Option::is_none")]
1998 pub class_code: Option<String>,
1999 /// `1` buy, `2` sell.
2000 #[serde(skip_serializing_if = "Option::is_none")]
2001 pub side: Option<String>,
2002 /// Type of the order, a digit as text (see the struct docs).
2003 #[serde(skip_serializing_if = "Option::is_none")]
2004 pub order_type: Option<String>,
2005 /// Average execution price. For repo and spread deals, the price of the first part.
2006 #[serde(skip_serializing_if = "Option::is_none")]
2007 pub average_price: Option<Decimal>,
2008 /// Id of the order at the exchange.
2009 #[serde(skip_serializing_if = "Option::is_none")]
2010 pub order_id: Option<String>,
2011 /// Id of the trade (fill).
2012 #[serde(skip_serializing_if = "Option::is_none")]
2013 pub execution_id: Option<String>,
2014 /// Price of the order.
2015 #[serde(skip_serializing_if = "Option::is_none")]
2016 pub price: Option<Decimal>,
2017 /// Currency.
2018 #[serde(skip_serializing_if = "Option::is_none")]
2019 pub currency: Option<String>,
2020 /// Client code.
2021 #[serde(skip_serializing_if = "Option::is_none")]
2022 pub client_code: Option<String>,
2023 /// Time of the transaction (UTC). `None` if absent or the epoch.
2024 #[serde(default, with = "date_format::option")]
2025 pub transaction_time: Option<DateTime<Utc>>,
2026 /// Order number.
2027 #[serde(skip_serializing_if = "Option::is_none")]
2028 pub order_number: Option<String>,
2029 /// Accrued coupon income.
2030 #[serde(skip_serializing_if = "Option::is_none")]
2031 pub accrued_coupon: Option<Decimal>,
2032 /// Volume of the trade.
2033 #[serde(skip_serializing_if = "Option::is_none")]
2034 pub execution_value: Option<Decimal>,
2035 /// Commission.
2036 #[serde(skip_serializing_if = "Option::is_none")]
2037 pub commission: Option<Decimal>,
2038 /// Exchange id.
2039 #[serde(skip_serializing_if = "Option::is_none")]
2040 pub security_exchange: Option<String>,
2041 // Fields of the current docs (GET /orders, sync methods).
2042 /// Why the order was rejected, when its status is `8`.
2043 #[serde(default, skip_serializing_if = "Option::is_none")]
2044 pub reject_reason: Option<String>,
2045 /// Time in force: `1` Day, `2` Fill Or Kill, `3` Good Till Date. Shown for market, limit and algorithmic orders.
2046 #[serde(default, skip_serializing_if = "Option::is_none")]
2047 pub time_in_force: Option<i32>,
2048 /// End date `YYYYMMDD`, shown when `time_in_force` is 3.
2049 #[serde(default, skip_serializing_if = "Option::is_none")]
2050 pub expiry_date: Option<String>,
2051 /// Stop price of a conditional order.
2052 #[serde(default, skip_serializing_if = "Option::is_none")]
2053 pub stop_price: Option<Decimal>,
2054 /// Take-profit price of a conditional order.
2055 #[serde(default, skip_serializing_if = "Option::is_none")]
2056 pub take_price: Option<Decimal>,
2057 /// Shown quantity of an iceberg order.
2058 #[serde(default, skip_serializing_if = "Option::is_none")]
2059 pub visible: Option<i64>,
2060 /// Protective spread of a conditional order.
2061 #[serde(default, skip_serializing_if = "Option::is_none")]
2062 pub spread: Option<Decimal>,
2063 /// Unit of `spread`: `0` price currency, `1` percent.
2064 #[serde(default, skip_serializing_if = "Option::is_none")]
2065 pub spread_type: Option<i32>,
2066 /// Offset of a conditional order.
2067 #[serde(default, skip_serializing_if = "Option::is_none")]
2068 pub offset: Option<Decimal>,
2069 /// Unit of `offset`: `0` price currency, `1` percent.
2070 #[serde(default, skip_serializing_if = "Option::is_none")]
2071 pub offset_type: Option<i32>,
2072}
2073
2074// ─── portfolio ────────────────────────────────────────────────────────────────
2075
2076/// One record of the portfolio: a securities position or a currency balance.
2077///
2078/// Records come in several kinds (see [`PortfolioPosition::position_type`]) and each kind fills only the
2079/// fields that fit, so all but `type` are optional. Money values come in the price currency and also in
2080/// roubles, dollars and euros. "P/L" fields are the change since the position was opened.
2081///
2082/// # Examples
2083///
2084/// ```
2085/// # use rust_decimal::Decimal;
2086/// # use rust_decimal_macros::dec;
2087/// use brokapi_bcs_protocol::http::models::PortfolioPosition;
2088///
2089/// let p: PortfolioPosition = serde_json::from_str(
2090/// r#"{"type":"depoLimit","ticker":"SBER","board":"TQBR","quantity":10,"currentPrice":244.0,
2091/// "currentValueRub":2440.0,"unrealizedPL":40.0,"unrealizedPercentPL":1.67}"#,
2092/// )?;
2093/// assert_eq!(p.quantity, Some(dec!(10.0)));
2094/// assert_eq!(p.unrealized_pl, Some(dec!(40.0)));
2095/// # Ok::<(), serde_json::Error>(())
2096/// ```
2097#[derive(Debug, Clone, Deserialize)]
2098#[serde(rename_all = "camelCase")]
2099pub struct PortfolioPosition {
2100 /// Kind of the record (JSON field `type`), for example `depoLimit` for a securities position and `moneyLimit` for a currency balance. The broker docs list no values.
2101 #[serde(rename = "type")]
2102 pub position_type: String,
2103 /// Id of the sub-account. Deprecated by the broker.
2104 #[serde(default)]
2105 pub sub_account_id: Option<String>,
2106 /// Id of the general agreement. Deprecated by the broker.
2107 #[serde(default)]
2108 pub agreement_id: Option<String>,
2109 /// Trading account.
2110 #[serde(default)]
2111 pub account: Option<String>,
2112 /// Exchange code.
2113 #[serde(default)]
2114 pub exchange: Option<String>,
2115 /// Ticker.
2116 #[serde(default)]
2117 pub ticker: Option<String>,
2118 /// Name of the instrument for display.
2119 #[serde(default)]
2120 pub display_name: Option<String>,
2121 /// Ticker of the base asset, for derivatives.
2122 #[serde(default)]
2123 pub base_asset_ticker: Option<String>,
2124 /// Price currency.
2125 #[serde(default)]
2126 pub currency: Option<String>,
2127 /// Top-level instrument type, as text. The broker docs list no values.
2128 #[serde(default)]
2129 pub upper_type: Option<String>,
2130 /// Instrument type, as text.
2131 #[serde(default)]
2132 pub instrument_type: Option<String>,
2133 /// Settlement term code.
2134 #[serde(default)]
2135 pub term: Option<String>,
2136 /// Quantity in pieces.
2137 #[serde(default)]
2138 pub quantity: Option<Decimal>,
2139 /// Quantity held by active orders or by collateral (initial margin).
2140 #[serde(default)]
2141 pub locked: Option<Decimal>,
2142 /// Balance price (the price the position was opened at), in the price currency.
2143 #[serde(default)]
2144 pub balance_price: Option<Decimal>,
2145 /// Current price.
2146 #[serde(default)]
2147 pub current_price: Option<Decimal>,
2148 /// Balance value (at the opening price), in the price currency.
2149 #[serde(default)]
2150 pub balance_value: Option<Decimal>,
2151 /// Balance value in roubles.
2152 #[serde(default)]
2153 pub balance_value_rub: Option<Decimal>,
2154 /// Balance value in dollars.
2155 #[serde(default)]
2156 pub balance_value_usd: Option<Decimal>,
2157 /// Balance value in euros.
2158 #[serde(default)]
2159 pub balance_value_eur: Option<Decimal>,
2160 /// Current value in the price currency.
2161 #[serde(default)]
2162 pub current_value: Option<Decimal>,
2163 /// Current value in roubles.
2164 #[serde(default)]
2165 pub current_value_rub: Option<Decimal>,
2166 /// Current value in dollars.
2167 #[serde(default)]
2168 pub current_value_usd: Option<Decimal>,
2169 /// Current value in euros.
2170 #[serde(default)]
2171 pub current_value_eur: Option<Decimal>,
2172 /// Price change since the position was opened (JSON field `unrealizedPL`, with capital letters).
2173 #[serde(default, rename = "unrealizedPL", alias = "unrealizedPl")]
2174 pub unrealized_pl: Option<Decimal>,
2175 /// The same change in percent (JSON field `unrealizedPercentPL`).
2176 #[serde(default, rename = "unrealizedPercentPL", alias = "unrealizedPercentPl")]
2177 pub unrealized_percent_pl: Option<f64>,
2178 /// Price change over the day (JSON field `dailyPL`). The broker docs word it as "daily change of the price since the position was opened".
2179 #[serde(default, rename = "dailyPL", alias = "dailyPl")]
2180 pub daily_pl: Option<Decimal>,
2181 /// The same change in percent (JSON field `dailyPercentPL`).
2182 #[serde(default, rename = "dailyPercentPL", alias = "dailyPercentPl")]
2183 pub daily_percent_pl: Option<f64>,
2184 /// Share of the portfolio in percent, of its value in roubles.
2185 #[serde(default)]
2186 pub portfolio_share: Option<f64>,
2187 /// Number of decimal places of the price.
2188 #[serde(default)]
2189 pub scale: Option<u32>,
2190 /// Minimum price step.
2191 #[serde(default)]
2192 pub minimum_step: Option<Decimal>,
2193 /// Class code (board) of the instrument.
2194 #[serde(default)]
2195 pub board: Option<String>,
2196 /// Unit of the price.
2197 #[serde(default)]
2198 pub price_unit: Option<String>,
2199 /// Face value, for bonds.
2200 #[serde(default)]
2201 pub face_value: Option<Decimal>,
2202 /// Accrued coupon income, for bonds.
2203 #[serde(default)]
2204 pub accrued_income: Option<Decimal>,
2205 /// Link to a logo. Deprecated by the broker.
2206 #[serde(default)]
2207 pub logo_link: Option<String>,
2208 /// `true` if the class is blocked.
2209 #[serde(default)]
2210 pub is_blocked: Option<bool>,
2211 /// `true` if the trading account is blocked.
2212 #[serde(default)]
2213 pub is_blocked_trade_account: Option<bool>,
2214 /// Amount reserved as collateral (initial margin) on futures.
2215 #[serde(default)]
2216 pub locked_for_futures: Option<Decimal>,
2217 /// Quantity in a lot.
2218 #[serde(default)]
2219 pub ratio_quantity: Option<Decimal>,
2220 /// Expiration date, kept as text.
2221 #[serde(default)]
2222 pub expire_date: Option<String>,
2223}
2224
2225/// The answer of [`BcsHttpClient::get_portfolio`](crate::http::client::BcsHttpClient::get_portfolio): a list of positions.
2226pub type PortfolioResponse = Vec<PortfolioPosition>;
2227
2228// ─── trades ───────────────────────────────────────────────────────────────────
2229
2230/// Body of [`BcsHttpClient::search_trades`](crate::http::client::BcsHttpClient::search_trades).
2231///
2232/// The period is text in ISO 8601 form (for example `2026-10-19T00:00:00.000Z`, UTC). The broker also has
2233/// filters by trade number and class code that this struct does not send.
2234///
2235/// # Examples
2236///
2237/// ```
2238/// use brokapi_bcs_protocol::http::models::TradeSearchRequest;
2239///
2240/// let req = TradeSearchRequest {
2241/// start_date_time: "2026-10-19T00:00:00.000Z".into(),
2242/// end_date_time: "2026-10-20T00:00:00.000Z".into(),
2243/// side: Some("1".into()),
2244/// tickers: None,
2245/// };
2246/// assert_eq!(serde_json::to_value(&req)?["side"], "1");
2247/// # Ok::<(), serde_json::Error>(())
2248/// ```
2249#[derive(Debug, Clone, Serialize)]
2250#[serde(rename_all = "camelCase")]
2251pub struct TradeSearchRequest {
2252 /// Start of the period, ISO 8601 text (UTC).
2253 pub start_date_time: String,
2254 /// End of the period, ISO 8601 text (UTC).
2255 pub end_date_time: String,
2256 /// `1` buy, `2` sell. Both if `None`.
2257 #[serde(skip_serializing_if = "Option::is_none")]
2258 pub side: Option<String>,
2259 /// Only these tickers.
2260 #[serde(skip_serializing_if = "Option::is_none")]
2261 pub tickers: Option<Vec<String>>,
2262}
2263
2264/// One own trade (fill) in the trades list.
2265///
2266/// The current docs give a **flat** record (the trade fields sit in the record). The `client_order_id` and
2267/// `data` fields belong to an older shape and are `None` against a broker that follows the docs. A record
2268/// holds the exchange order number and the trade number, but not your `clientOrderId` and no commission.
2269/// Quantity comes in pieces and in lots.
2270///
2271/// # Examples
2272///
2273/// ```
2274/// # use rust_decimal::Decimal;
2275/// # use rust_decimal_macros::dec;
2276/// use brokapi_bcs_protocol::http::models::TradeRecord;
2277///
2278/// let t: TradeRecord = serde_json::from_str(
2279/// r#"{"orderNum":79000001,"tradeNum":5,"ticker":"SBER","classCode":"TQBR","side":"1",
2280/// "price":300.0,"volume":3000.0,"tradeQuantity":10,"tradeDateTime":"2026-10-19T07:05:00.000Z"}"#,
2281/// )?;
2282/// assert_eq!(t.trade_quantity, Some(dec!(10.0)));
2283/// # Ok::<(), serde_json::Error>(())
2284/// ```
2285#[derive(Debug, Clone, Deserialize)]
2286#[serde(rename_all = "camelCase")]
2287pub struct TradeRecord {
2288 /// Your order id. Older shape only: the current docs record has none.
2289 #[serde(default)]
2290 pub client_order_id: Option<String>,
2291 /// A nested report. Older shape only: the current docs record is flat.
2292 #[serde(default)]
2293 pub data: Option<TradeData>,
2294 // The flat record of the docs (`POST /trades/search`, schema `TradesSearchResponse`, checked
2295 // 2026-10-19): the trade fields sit in the record itself, there is no `data` wrapper and no
2296 // `clientOrderId`. The `data` form above is what the first version of this model assumed.
2297 /// Number of the order at the exchange.
2298 #[serde(default)]
2299 pub order_num: Option<i64>,
2300 /// Number of the trade.
2301 #[serde(default)]
2302 pub trade_num: Option<i64>,
2303 /// Ticker.
2304 #[serde(default)]
2305 pub ticker: Option<String>,
2306 /// Class code (board).
2307 #[serde(default)]
2308 pub class_code: Option<String>,
2309 /// `1` buy, `2` sell.
2310 #[serde(default)]
2311 pub side: Option<String>,
2312 /// Trade price.
2313 #[serde(default)]
2314 pub price: Option<Decimal>,
2315 /// Trade volume in the currency.
2316 #[serde(default)]
2317 pub volume: Option<Decimal>,
2318 /// Quantity in pieces.
2319 #[serde(default)]
2320 pub trade_quantity: Option<Decimal>,
2321 /// Quantity in lots.
2322 #[serde(default)]
2323 pub trade_quantity_lots: Option<Decimal>,
2324 /// Time of the trade (UTC), kept as the text the broker sends.
2325 #[serde(default)]
2326 pub trade_date_time: Option<String>,
2327}
2328
2329/// The nested report of an older trade record shape. See [`TradeRecord`].
2330///
2331/// It has the fields of an order report. Prefer the flat fields of [`TradeRecord`].
2332#[derive(Debug, Clone, Deserialize)]
2333#[serde(rename_all = "camelCase")]
2334pub struct TradeData {
2335 /// Type of the message, as text.
2336 #[serde(default)]
2337 pub message_type: Option<String>,
2338 /// State of the order, a digit as text.
2339 #[serde(default)]
2340 pub order_status: Option<String>,
2341 /// Type of the report, a digit as text.
2342 #[serde(default)]
2343 pub execution_type: Option<String>,
2344 /// Quantity of the order, in pieces.
2345 #[serde(default)]
2346 pub order_quantity: Option<Decimal>,
2347 /// Executed quantity, in pieces.
2348 #[serde(default)]
2349 pub executed_quantity: Option<Decimal>,
2350 /// Quantity of the latest fill, in pieces.
2351 #[serde(default)]
2352 pub last_quantity: Option<Decimal>,
2353 /// Remaining quantity, in pieces.
2354 #[serde(default)]
2355 pub remained_quantity: Option<Decimal>,
2356 /// Ticker.
2357 #[serde(default)]
2358 pub ticker: Option<String>,
2359 /// Class code (board).
2360 #[serde(default)]
2361 pub class_code: Option<String>,
2362 /// `1` buy, `2` sell.
2363 #[serde(default)]
2364 pub side: Option<String>,
2365 /// Type of the order, a digit as text.
2366 #[serde(default)]
2367 pub order_type: Option<String>,
2368 /// Average execution price.
2369 #[serde(default)]
2370 pub average_price: Option<Decimal>,
2371 /// Id of the order at the exchange.
2372 #[serde(default)]
2373 pub order_id: Option<String>,
2374 /// Id of the trade.
2375 #[serde(default)]
2376 pub execution_id: Option<String>,
2377 /// Price.
2378 #[serde(default)]
2379 pub price: Option<Decimal>,
2380 /// Currency.
2381 #[serde(default)]
2382 pub currency: Option<String>,
2383 /// Client code.
2384 #[serde(default)]
2385 pub client_code: Option<String>,
2386 /// Time of the transaction, kept as text.
2387 #[serde(default)]
2388 pub transaction_time: Option<String>,
2389 /// Order number.
2390 #[serde(default)]
2391 pub order_number: Option<String>,
2392 /// Accrued coupon income.
2393 #[serde(default)]
2394 pub accrued_coupon: Option<Decimal>,
2395 /// Volume of the trade.
2396 #[serde(default)]
2397 pub execution_value: Option<Decimal>,
2398 /// Commission.
2399 #[serde(default)]
2400 pub commission: Option<Decimal>,
2401 /// Exchange id.
2402 #[serde(default)]
2403 pub security_exchange: Option<String>,
2404}
2405
2406#[cfg(test)]
2407mod last_trades_tests {
2408 use super::*;
2409
2410 #[test]
2411 fn the_tape_is_read_in_both_shapes() {
2412 let wrapped: LastTradesWire = serde_json::from_str(
2413 r#"{"records":[{"dateTime":"2026-10-19T07:05:00.000Z","side":"1","price":300.1,"volume":60020,"quantity":200}]}"#,
2414 )
2415 .unwrap();
2416 let plain: LastTradesWire = serde_json::from_str(
2417 r#"[{"ticker":"SBER","classCode":"TQBR","side":"BUY","price":300.1,"quantity":200}]"#,
2418 )
2419 .unwrap();
2420 assert_eq!(wrapped.into_vec().len(), 1);
2421 assert_eq!(plain.into_vec()[0].ticker.as_deref(), Some("SBER"));
2422 }
2423
2424 /// The untagged wrapper buffers the numbers before it reads them. They must keep their digits
2425 /// there too: `244.30` is not an `f64`.
2426 #[test]
2427 fn the_untagged_wrapper_keeps_the_digits() {
2428 for body in [
2429 r#"[{"price":244.30,"quantity":5}]"#,
2430 r#"{"records":[{"price":244.30,"quantity":0.0001}]}"#,
2431 ] {
2432 let wire: LastTradesWire = serde_json::from_str(body).unwrap();
2433 let trades = wire.into_vec();
2434 assert_eq!(trades[0].price.to_string(), "244.30", "{body}");
2435 }
2436 }
2437
2438 #[test]
2439 fn request_sends_only_what_is_set() {
2440 let r = LastTradesRequest {
2441 ticker: "SBER".into(),
2442 class_code: "TQBR".into(),
2443 ..Default::default()
2444 };
2445 assert_eq!(
2446 serde_json::to_value(&r).unwrap(),
2447 serde_json::json!({"ticker":"SBER","classCode":"TQBR"})
2448 );
2449 }
2450}