Skip to main content

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}