Skip to main content

brokapi_bcs_protocol/ws/
models.rs

1//! Frames of the WebSocket channels: the commands you send and the data the broker sends.
2//!
3//! Market data frames are told apart by their `responseType` ([`MarketDataMessage`]). The other
4//! channels send a bare object or array, and the channel decides the type. All times from the broker
5//! are UTC. Quantities are in pieces.
6
7use crate::models::enums::{ExecutionType, OrderSide, OrderStatus, OrderType};
8use crate::models::instrument::InstrumentKey;
9use crate::utils::date_format;
10use chrono::{DateTime, Utc};
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13
14// ─── common ──────────────────────────────────────────────────────────────────
15
16/// An error frame of the broker (`"type": "error"`), the server to client direction.
17///
18/// A frame like this is parsed into [`IncomingMessage::Error`](crate::models::messages::IncomingMessage::Error).
19///
20/// # Examples
21///
22/// ```
23/// use brokapi_bcs_protocol::ws::models::WsError;
24///
25/// let e: WsError = serde_json::from_str(r#"{"type":"error","code":"UNAUTHORIZED","message":"no token"}"#)?;
26/// assert_eq!(e.code, "UNAUTHORIZED");
27/// # Ok::<(), serde_json::Error>(())
28/// ```
29#[derive(Debug, Clone, Deserialize)]
30pub struct WsError {
31    /// Kind of the frame (JSON field `type`), `error` for this type.
32    #[serde(rename = "type")]
33    pub msg_type: String,
34    /// Machine code of the error.
35    pub code: String,
36    /// Text of the error.
37    pub message: String,
38}
39
40/// The two fields of a frame that the parser looks at first to decide what kind of frame it is.
41///
42/// Every other field is ignored here. The parser then reads the whole frame into the matching type.
43#[derive(Debug, Clone, Deserialize)]
44pub struct WsRawMessage {
45    /// The `type` of the frame, if there is one. `error` marks an error frame.
46    #[serde(default, rename = "type")]
47    pub msg_type: Option<String>,
48    /// The `responseType` of the frame, if there is one. Market data frames have it.
49    #[serde(default, rename = "responseType")]
50    pub response_type: Option<String>,
51}
52
53// ─── subscription ─────────────────────────────────────────────────────────────
54
55/// A subscribe or unsubscribe command for the market data channel (client to server).
56///
57/// Build it with the constructors: [`MarketDataSubscribe::quotes`], [`MarketDataSubscribe::order_book`],
58/// [`MarketDataSubscribe::last_trades`], [`MarketDataSubscribe::last_candle`] and the `unsubscribe_*` forms.
59/// `subscribe_type` and `data_type` are numbers on the wire, as in the broker docs. The broker allows
60/// 100 instruments per connection and 10 client messages per second.
61///
62/// # Examples
63///
64/// ```
65/// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
66/// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
67///
68/// let sub = MarketDataSubscribe::order_book(vec![InstrumentKey::new("SBER", "TQBR")], 20);
69/// assert_eq!(
70///     serde_json::to_value(&sub)?,
71///     serde_json::json!({
72///         "subscribeType": 0,
73///         "dataType": 0,
74///         "instruments": [{ "ticker": "SBER", "classCode": "TQBR" }],
75///         "depth": 20
76///     })
77/// );
78/// # Ok::<(), serde_json::Error>(())
79/// ```
80#[derive(Debug, Clone, PartialEq, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub struct MarketDataSubscribe {
83    /// `0` subscribe, `1` unsubscribe.
84    pub subscribe_type: u8,
85    /// What to subscribe to: `0` order book, `1` last candle, `2` anonymous trades, `3` quotes.
86    pub data_type: u8,
87    /// The instruments the command is about.
88    pub instruments: Vec<InstrumentKey>,
89    /// Order book depth, 1 to 20 by the broker docs (default 20). Sent only for the order book. Not checked here.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub depth: Option<u32>,
92    /// Candle time frame: `M1 M5 M15 M30 H1 H4 D W MN`. Sent only for the last candle. Not checked here.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub time_frame: Option<String>,
95}
96
97impl MarketDataSubscribe {
98    /// Turns a subscribe command into the matching unsubscribe: the same data type, instruments, depth
99    /// and time frame, with `subscribe_type` set to `1`.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
105    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
106    ///
107    /// let sub = MarketDataSubscribe::order_book(vec![InstrumentKey::new("SBER", "TQBR")], 10);
108    /// let unsub = sub.clone().into_unsubscribe();
109    /// assert_eq!((sub.subscribe_type, unsub.subscribe_type), (0, 1));
110    /// assert_eq!(unsub.depth, Some(10));
111    /// ```
112    pub fn into_unsubscribe(self) -> Self {
113        Self {
114            subscribe_type: 1,
115            ..self
116        }
117    }
118
119    /// Subscribes to quotes (`dataType` 3).
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
125    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
126    ///
127    /// let sub = MarketDataSubscribe::quotes(vec![InstrumentKey::new("SBER", "TQBR")]);
128    /// assert_eq!((sub.subscribe_type, sub.data_type), (0, 3));
129    /// ```
130    pub fn quotes(instruments: Vec<InstrumentKey>) -> Self {
131        Self {
132            subscribe_type: 0,
133            data_type: 3,
134            instruments,
135            depth: None,
136            time_frame: None,
137        }
138    }
139
140    /// Unsubscribes from quotes (`dataType` 3, `subscribeType` 1).
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
146    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
147    ///
148    /// let cmd = MarketDataSubscribe::unsubscribe_quotes(vec![InstrumentKey::new("SBER", "TQBR")]);
149    /// assert_eq!((cmd.subscribe_type, cmd.data_type), (1, 3));
150    /// ```
151    pub fn unsubscribe_quotes(instruments: Vec<InstrumentKey>) -> Self {
152        Self {
153            subscribe_type: 1,
154            data_type: 3,
155            instruments,
156            depth: None,
157            time_frame: None,
158        }
159    }
160
161    /// Subscribes to the order book (`dataType` 0) with `depth` levels per side.
162    ///
163    /// The broker docs allow 1 to 20 and default to 20. The depth is not checked here. The broker
164    /// computes the volumes in the answers over the levels within `depth`.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
170    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
171    ///
172    /// let sub = MarketDataSubscribe::order_book(vec![InstrumentKey::new("SBER", "TQBR")], 5);
173    /// assert_eq!((sub.data_type, sub.depth), (0, Some(5)));
174    /// ```
175    pub fn order_book(instruments: Vec<InstrumentKey>, depth: u32) -> Self {
176        Self {
177            subscribe_type: 0,
178            data_type: 0,
179            instruments,
180            depth: Some(depth),
181            time_frame: None,
182        }
183    }
184
185    /// Unsubscribes from the order book (`dataType` 0, `subscribeType` 1). Pass the same `depth` as
186    /// at subscribe if you compare it with the restore list of a session.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
192    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
193    ///
194    /// let cmd = MarketDataSubscribe::unsubscribe_order_book(vec![InstrumentKey::new("SBER", "TQBR")], 5);
195    /// assert_eq!((cmd.subscribe_type, cmd.data_type), (1, 0));
196    /// ```
197    pub fn unsubscribe_order_book(instruments: Vec<InstrumentKey>, depth: u32) -> Self {
198        Self {
199            subscribe_type: 1,
200            data_type: 0,
201            instruments,
202            depth: Some(depth),
203            time_frame: None,
204        }
205    }
206
207    /// Subscribes to the anonymous trades tape (`dataType` 2).
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
213    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
214    ///
215    /// let sub = MarketDataSubscribe::last_trades(vec![InstrumentKey::new("SBER", "TQBR")]);
216    /// assert_eq!(sub.data_type, 2);
217    /// ```
218    pub fn last_trades(instruments: Vec<InstrumentKey>) -> Self {
219        Self {
220            subscribe_type: 0,
221            data_type: 2,
222            instruments,
223            depth: None,
224            time_frame: None,
225        }
226    }
227
228    /// Unsubscribes from the anonymous trades tape (`dataType` 2, `subscribeType` 1).
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
234    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
235    ///
236    /// let cmd = MarketDataSubscribe::unsubscribe_last_trades(vec![InstrumentKey::new("SBER", "TQBR")]);
237    /// assert_eq!((cmd.subscribe_type, cmd.data_type), (1, 2));
238    /// ```
239    pub fn unsubscribe_last_trades(instruments: Vec<InstrumentKey>) -> Self {
240        Self {
241            subscribe_type: 1,
242            data_type: 2,
243            instruments,
244            depth: None,
245            time_frame: None,
246        }
247    }
248
249    /// Subscribes to the last candle (`dataType` 1) for a time frame.
250    ///
251    /// `time_frame` is one of `M1 M5 M15 M30 H1 H4 D W MN` (broker docs). It is not checked here.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
257    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
258    ///
259    /// let sub = MarketDataSubscribe::last_candle(vec![InstrumentKey::new("SBER", "TQBR")], "M5");
260    /// assert_eq!((sub.data_type, sub.time_frame.as_deref()), (1, Some("M5")));
261    /// ```
262    pub fn last_candle(instruments: Vec<InstrumentKey>, time_frame: &str) -> Self {
263        Self {
264            subscribe_type: 0,
265            data_type: 1,
266            instruments,
267            depth: None,
268            time_frame: Some(time_frame.to_string()),
269        }
270    }
271
272    /// Unsubscribes from the last candle (`dataType` 1, `subscribeType` 1) for a time frame.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
278    /// use brokapi_bcs_protocol::ws::models::MarketDataSubscribe;
279    ///
280    /// let cmd = MarketDataSubscribe::unsubscribe_last_candle(vec![InstrumentKey::new("SBER", "TQBR")], "M5");
281    /// assert_eq!(cmd.subscribe_type, 1);
282    /// ```
283    pub fn unsubscribe_last_candle(instruments: Vec<InstrumentKey>, time_frame: &str) -> Self {
284        Self {
285            subscribe_type: 1,
286            data_type: 1,
287            instruments,
288            depth: None,
289            time_frame: Some(time_frame.to_string()),
290        }
291    }
292}
293
294/// A subscription confirmation in the flat form (server to client).
295///
296/// The parser does not produce this type: confirmations inside [`MarketDataMessage`] use
297/// [`SubscribeSuccessPayload`]. It is kept for code that parses a frame itself.
298#[derive(Debug, Clone, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct SubscribeSuccess {
301    /// Kind of the confirmation, for example `OrderBookSuccess`.
302    pub response_type: String,
303    /// The instruments the subscription covers, if the broker lists them.
304    #[serde(default)]
305    pub instruments: Vec<InstrumentKey>,
306    /// Order book depth, for an order book subscription.
307    #[serde(default)]
308    pub depth: Option<u32>,
309    /// Candle time frame, for a candle subscription.
310    #[serde(default)]
311    pub time_frame: Option<String>,
312}
313
314// ─── outgoing ─────────────────────────────────────────────────────────────────
315
316/// A command a session sends to the broker. It serializes as the plain command, without a wrapper.
317///
318/// The two variants hold the same type: the difference is only the purpose, and the session turns an
319/// unsubscribe into a command with `subscribeType = 1`.
320///
321/// # Examples
322///
323/// ```
324/// use brokapi_bcs_protocol::models::instrument::InstrumentKey;
325/// use brokapi_bcs_protocol::ws::models::{MarketDataSubscribe, WsOutgoing};
326///
327/// let keys = vec![InstrumentKey::new("SBER", "TQBR")];
328/// let msg = WsOutgoing::Subscribe(MarketDataSubscribe::quotes(keys));
329/// let json = serde_json::to_value(&msg)?;
330/// assert_eq!(json["dataType"], 3);
331/// assert_eq!(json["subscribeType"], 0);
332/// # Ok::<(), serde_json::Error>(())
333/// ```
334#[derive(Debug, Clone, PartialEq, Serialize)]
335#[serde(untagged)]
336pub enum WsOutgoing {
337    /// A subscribe command.
338    Subscribe(MarketDataSubscribe),
339    /// An unsubscribe command.
340    Unsubscribe(MarketDataSubscribe),
341}
342
343// ─── market_data ──────────────────────────────────────────────────────────────
344
345/// A frame of the market data channel, told apart by its `responseType`.
346///
347/// The success variants confirm a subscription. The data variants carry the data. A value of
348/// `responseType` this library does not know reads as [`MarketDataMessage::Undefined`].
349///
350/// **Error frames of the documented shape do not arrive as `ErrorResponse`.** The docs show an error as
351/// a frame with the `responseType` of the data (for example `OrderBook`) and an `errors` list. Such a frame
352/// does not fit the data type, so the parser returns it as
353/// [`IncomingMessage::Unknown`](crate::models::messages::IncomingMessage::Unknown) with the raw text. The
354/// `ErrorResponse` variant is for a frame that really has `responseType = "ErrorResponse"`.
355///
356/// # Examples
357///
358/// ```
359/// # use rust_decimal::Decimal;
360/// # use rust_decimal_macros::dec;
361/// use brokapi_bcs_protocol::ws::models::MarketDataMessage;
362///
363/// let frame = r#"{"responseType":"LastTrades","ticker":"SBER","classCode":"TQBR","side":"BUY","price":300.15,"quantity":100}"#;
364/// match serde_json::from_str::<MarketDataMessage>(frame)? {
365///     MarketDataMessage::LastTrades(t) => assert_eq!(t.quantity, dec!(100.0)),
366///     other => panic!("unexpected: {other:?}"),
367/// }
368/// # Ok::<(), serde_json::Error>(())
369/// ```
370#[derive(Debug, Clone, Deserialize)]
371#[serde(tag = "responseType")]
372pub enum MarketDataMessage {
373    /// The subscription to quotes was accepted.
374    QuotesSuccess(SubscribeSuccessPayload),
375    /// A quote update.
376    Quotes(QuotesData),
377    /// The subscription to the order book was accepted.
378    OrderBookSuccess(SubscribeSuccessPayload),
379    /// An order book update.
380    OrderBook(OrderBookData),
381    /// The subscription to the trades tape was accepted.
382    LastTradesSuccess(SubscribeSuccessPayload),
383    /// One trade of the tape.
384    LastTrades(LastTradesData),
385    /// The subscription to the last candle was accepted (`responseType` is `CandleStickSuccess`).
386    #[serde(rename = "CandleStickSuccess")]
387    LastCandleSuccess(SubscribeSuccessPayload),
388    /// An update of the last candle (`responseType` is `CandleStick`).
389    #[serde(rename = "CandleStick")]
390    LastCandle(LastCandleData),
391    /// An error frame with `responseType = "ErrorResponse"`. See the note on the type.
392    ErrorResponse(MarketDataError),
393    /// A `responseType` this library does not know.
394    #[serde(other)]
395    Undefined,
396}
397
398/// The body of a subscription confirmation.
399///
400/// The docs show the instrument as flat `ticker` and `classCode` fields in the confirmation. This type
401/// reads an `instruments` list. If the broker sends the flat form, `instruments` stays empty. Not verified
402/// against the live API which form the broker uses.
403#[derive(Debug, Clone, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct SubscribeSuccessPayload {
406    /// The instruments the subscription covers, if the broker lists them. Empty otherwise.
407    #[serde(default)]
408    pub instruments: Vec<InstrumentKey>,
409    /// Order book depth, for an order book subscription.
410    #[serde(default)]
411    pub depth: Option<u32>,
412    /// Candle time frame, for a candle subscription.
413    #[serde(default)]
414    pub time_frame: Option<String>,
415}
416
417/// A quote update of one instrument.
418///
419/// The current docs name the best sell price `offer` and the last price `last`. Older samples used `ask`
420/// and `lastPrice`. Both spellings are read into [`QuotesData::ask`] and [`QuotesData::last_price`].
421/// The docs give no quantity at the best prices.
422///
423/// # Examples
424///
425/// ```
426/// # use rust_decimal::Decimal;
427/// # use rust_decimal_macros::dec;
428/// use brokapi_bcs_protocol::ws::models::QuotesData;
429///
430/// // The current docs form.
431/// let q: QuotesData = serde_json::from_str(
432///     r#"{"ticker":"SBER","classCode":"TQBR","bid":305.86,"offer":305.96,"last":305.97}"#,
433/// )?;
434/// assert_eq!((q.bid, q.ask, q.last_price), (Some(dec!(305.86)), Some(dec!(305.96)), Some(dec!(305.97))));
435///
436/// // The older form.
437/// let old: QuotesData = serde_json::from_str(r#"{"ticker":"SBER","classCode":"TQBR","ask":305.96,"lastPrice":305.97}"#)?;
438/// assert_eq!(old.ask, Some(dec!(305.96)));
439/// # Ok::<(), serde_json::Error>(())
440/// ```
441#[derive(Debug, Clone, Deserialize)]
442#[serde(rename_all = "camelCase")]
443pub struct QuotesData {
444    /// Ticker.
445    pub ticker: String,
446    /// Class code (board).
447    pub class_code: String,
448    /// Time the data was updated (UTC), kept as the text the broker sends.
449    #[serde(default)]
450    pub date_time: Option<String>,
451    /// Best buy price.
452    #[serde(default)]
453    pub bid: Option<Decimal>,
454    /// Best sell price. The broker docs call the field `offer`, older samples `ask`. Both are read.
455    #[serde(default, alias = "offer")]
456    pub ask: Option<Decimal>,
457    /// Price of the last trade. The broker docs call the field `last`, older samples `lastPrice`. Both are read.
458    #[serde(default, alias = "last")]
459    pub last_price: Option<Decimal>,
460    /// Opening price of the session.
461    #[serde(default)]
462    pub open: Option<Decimal>,
463    /// Closing price of the previous session.
464    #[serde(default)]
465    pub close: Option<Decimal>,
466    /// Highest price of the day.
467    #[serde(default)]
468    pub high: Option<Decimal>,
469    /// Lowest price of the day.
470    #[serde(default)]
471    pub low: Option<Decimal>,
472    /// Price change over the session, in the price currency.
473    #[serde(default)]
474    pub change: Option<Decimal>,
475    /// Price change over the session, in percent.
476    #[serde(default)]
477    pub change_rate: Option<f64>,
478    /// Traded value. Not in the current docs, read if the broker sends it.
479    #[serde(default)]
480    pub value: Option<Decimal>,
481    /// Traded volume. Not in the current docs, read if the broker sends it.
482    #[serde(default)]
483    pub volume: Option<Decimal>,
484    /// Number of trades. Not in the current docs, read if the broker sends it.
485    #[serde(default)]
486    pub trade_num: Option<u64>,
487}
488
489/// One price level of the order book: a price and the quantity at it.
490#[derive(Debug, Clone, Deserialize)]
491pub struct OrderBookLevel {
492    /// Price of the level.
493    pub price: Decimal,
494    /// Quantity at the level, in pieces.
495    pub quantity: Decimal,
496}
497
498/// An order book update. It is a full snapshot of the levels within `depth`, not a delta.
499///
500/// `bids` and `asks` come best price first, as the broker sends them. The code does not sort them.
501///
502/// # Examples
503///
504/// ```
505/// # use rust_decimal::Decimal;
506/// # use rust_decimal_macros::dec;
507/// use brokapi_bcs_protocol::ws::models::OrderBookData;
508///
509/// let book: OrderBookData = serde_json::from_str(
510///     r#"{"ticker":"SBER","classCode":"TQBR","depth":2,
511///         "bids":[{"price":244.30,"quantity":100}],"asks":[{"price":244.35,"quantity":120}]}"#,
512/// )?;
513/// assert_eq!(book.bids[0].price, dec!(244.30));
514/// # Ok::<(), serde_json::Error>(())
515/// ```
516#[derive(Debug, Clone, Deserialize)]
517#[serde(rename_all = "camelCase")]
518pub struct OrderBookData {
519    /// Ticker.
520    pub ticker: String,
521    /// Class code (board).
522    pub class_code: String,
523    /// Depth of the book.
524    #[serde(default)]
525    pub depth: Option<u32>,
526    /// Time the book was updated (UTC), kept as the text the broker sends.
527    #[serde(default)]
528    pub date_time: Option<String>,
529    /// Buy side, best price first.
530    #[serde(default)]
531    pub bids: Vec<OrderBookLevel>,
532    /// Sell side, best price first.
533    #[serde(default)]
534    pub asks: Vec<OrderBookLevel>,
535}
536
537/// One trade of the anonymous trades tape: one trade per frame.
538///
539/// # Examples
540///
541/// ```
542/// use brokapi_bcs_protocol::ws::models::LastTradesData;
543///
544/// let t: LastTradesData = serde_json::from_str(
545///     r#"{"ticker":"SBER","classCode":"TQBR","side":"SELL","volume":30015.0,"price":300.15,"quantity":100}"#,
546/// )?;
547/// assert_eq!(t.side.as_deref(), Some("SELL"));
548/// # Ok::<(), serde_json::Error>(())
549/// ```
550#[derive(Debug, Clone, Deserialize)]
551#[serde(rename_all = "camelCase")]
552pub struct LastTradesData {
553    /// Ticker.
554    pub ticker: String,
555    /// Class code (board).
556    pub class_code: String,
557    /// Side of the trade as text: `BUY` or `SELL` (the REST tape uses `1` and `2`).
558    #[serde(default)]
559    pub side: Option<String>,
560    /// Trade volume in the instrument currency.
561    #[serde(default)]
562    pub volume: Option<Decimal>,
563    /// Trade price.
564    pub price: Decimal,
565    /// Quantity in pieces.
566    pub quantity: Decimal,
567    /// Time of the trade (UTC), kept as the text the broker sends.
568    #[serde(default)]
569    pub date_time: Option<String>,
570}
571
572/// An update of the last candle for a time frame.
573///
574/// # Examples
575///
576/// ```
577/// # use rust_decimal::Decimal;
578/// # use rust_decimal_macros::dec;
579/// use brokapi_bcs_protocol::ws::models::LastCandleData;
580///
581/// let c: LastCandleData = serde_json::from_str(
582///     r#"{"ticker":"SBER","classCode":"TQBR","timeFrame":"M1","open":244.2,"close":244.5,
583///         "high":244.7,"low":243.9,"volume":3200,"dateTime":"2024-11-10T10:30:00.000Z"}"#,
584/// )?;
585/// assert_eq!(c.close, Some(dec!(244.5)));
586/// assert_eq!(c.timestamp.as_deref(), Some("2024-11-10T10:30:00.000Z"));
587/// # Ok::<(), serde_json::Error>(())
588/// ```
589#[derive(Debug, Clone, Deserialize)]
590#[serde(rename_all = "camelCase")]
591pub struct LastCandleData {
592    /// Ticker.
593    #[serde(default)]
594    pub ticker: Option<String>,
595    /// Class code (board).
596    #[serde(default)]
597    pub class_code: Option<String>,
598    /// Time frame of the candle: `M1 M5 M15 M30 H1 H4 D W MN`.
599    #[serde(default)]
600    pub time_frame: Option<String>,
601    /// Time of the candle (UTC), kept as text (JSON field `dateTime`).
602    #[serde(default, rename = "dateTime")]
603    pub timestamp: Option<String>,
604    /// Opening price.
605    #[serde(default)]
606    pub open: Option<Decimal>,
607    /// Closing price.
608    #[serde(default)]
609    pub close: Option<Decimal>,
610    /// Highest price.
611    #[serde(default)]
612    pub high: Option<Decimal>,
613    /// Lowest price.
614    #[serde(default)]
615    pub low: Option<Decimal>,
616    /// Traded volume of the candle, in the currency.
617    #[serde(default)]
618    pub volume: Option<Decimal>,
619}
620
621/// The error part of a market data frame.
622///
623/// Either `error` (one) or `errors` (a list) can be set. The broker docs show the list.
624#[derive(Debug, Clone, Deserialize)]
625pub struct MarketDataError {
626    /// A single error, if the frame has one.
627    #[serde(default)]
628    pub error: Option<MarketDataErrorDetail>,
629    /// The errors of the frame. Empty if there are none.
630    #[serde(default)]
631    pub errors: Vec<MarketDataErrorDetail>,
632}
633
634/// One error of a market data frame.
635///
636/// The docs list these codes: `NO_DATE` (no data), `NOT_FOUND` (instrument not found), `INCORRECT_JSON`
637/// (invalid JSON), `BAD_REQUEST` (execution error), `UNAUTHORIZED` (client not authorized).
638#[derive(Debug, Clone, Deserialize)]
639pub struct MarketDataErrorDetail {
640    /// Error code, see the type docs.
641    pub code: String,
642    /// Text of the error.
643    pub message: String,
644}
645
646// ─── orders ───────────────────────────────────────────────────────────────────
647
648fn unknown_side() -> OrderSide {
649    OrderSide::Unknown
650}
651
652/// A message about an order from the order channels.
653///
654/// It comes from `orders/events/ws` (the current channel) and from the deprecated execution and transaction
655/// channels. It carries the report in [`OrderMessage::data`].
656///
657/// # Examples
658///
659/// ```
660/// # use rust_decimal::Decimal;
661/// # use rust_decimal_macros::dec;
662/// use brokapi_bcs_protocol::models::enums::{ExecutionType, OrderStatus};
663/// use brokapi_bcs_protocol::ws::models::OrderMessage;
664///
665/// let m: OrderMessage = serde_json::from_str(
666///     r#"{"originalClientOrderId":"o-1","clientOrderId":"",
667///         "data":{"orderStatus":"1","executionType":"1","ticker":"SBER","classCode":"TQBR",
668///                 "side":"1","orderType":"2","orderQuantity":10,"executedQuantity":4,
669///                 "lastQuantity":4,"remainedQuantity":6,"price":300.0}}"#,
670/// )?;
671/// let data = m.data.unwrap();
672/// assert_eq!(data.order_status, OrderStatus::PartiallyFilled);
673/// assert_eq!(data.execution_type, Some(ExecutionType::PartialFill));
674/// assert_eq!(data.last_quantity, dec!(4.0));
675/// # Ok::<(), serde_json::Error>(())
676/// ```
677#[derive(Debug, Clone, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct OrderMessage {
680    /// The `clientOrderId` you gave when you placed the order. For an edit it is the id of the changed order, as the broker docs word it.
681    #[serde(default)]
682    pub original_client_order_id: Option<String>,
683    /// Id of the request that caused the message. Empty text if the broker sent none.
684    #[serde(default)]
685    pub client_order_id: String,
686    /// The report. `None` if the message has none.
687    #[serde(default)]
688    pub data: Option<OrderExecutionData>,
689}
690
691/// The report inside an [`OrderMessage`]: the state of the order and the last fill.
692///
693/// It is the WebSocket form of [`crate::http::models::OrderExecutionData`], with typed enums. Quantities
694/// are in pieces. Fields the broker leaves out in some messages read as `0` or `None`, noted per field.
695///
696/// * `execution_type` says why the message was sent. A fill has `last_quantity` above 0 and a trade
697///   id in `execution_id`.
698/// * This struct has no field for a reject reason. The REST report has `rejectReason`, but the events
699///   docs do not list it among the fields of `data`.
700#[derive(Debug, Clone, Deserialize)]
701#[serde(rename_all = "camelCase")]
702pub struct OrderExecutionData {
703    /// Type of the message, as text.
704    #[serde(default)]
705    pub message_type: Option<String>,
706    /// State of the order. Required: a message without it does not parse.
707    pub order_status: OrderStatus,
708    /// Why the message was sent.
709    #[serde(default)]
710    pub execution_type: Option<ExecutionType>,
711    /// Quantity of the order, in pieces.
712    #[serde(default)]
713    pub order_quantity: Option<Decimal>,
714    /// Executed quantity so far, in pieces.
715    #[serde(default)]
716    pub executed_quantity: Option<Decimal>,
717    /// Absent in pure status messages of the current `orders/events/ws` channel (no fill): `0`.
718    #[serde(default)]
719    pub last_quantity: Decimal,
720    /// Remaining quantity, in pieces. `0` if the message has none.
721    #[serde(default)]
722    pub remained_quantity: Decimal,
723    /// Ticker. Required.
724    pub ticker: String,
725    /// Class code (board). Empty text if absent.
726    #[serde(default)]
727    pub class_code: String,
728    /// Buy or sell. [`OrderSide::Unknown`] if absent.
729    #[serde(default = "unknown_side")]
730    pub side: OrderSide,
731    /// Type of the order. Types other than market and limit read as [`OrderType::Unknown`].
732    #[serde(default)]
733    pub order_type: Option<OrderType>,
734    /// Average execution price. For repo and spread deals, the price of the first part.
735    #[serde(default)]
736    pub average_price: Option<Decimal>,
737    /// Id of the order at the exchange.
738    #[serde(default)]
739    pub order_id: Option<String>,
740    /// Id of the trade (fill). Set when the message reports a fill.
741    #[serde(default)]
742    pub execution_id: Option<String>,
743    /// Absent in some cancel / reject messages: `0`.
744    #[serde(default)]
745    pub price: Decimal,
746    /// Currency.
747    #[serde(default)]
748    pub currency: Option<String>,
749    /// Client code.
750    #[serde(default)]
751    pub client_code: Option<String>,
752    /// Time of the transaction (UTC). `None` if absent or the epoch.
753    #[serde(default, with = "date_format::option")]
754    pub transaction_time: Option<DateTime<Utc>>,
755    /// Trade date, kept as text. For the evening session of the derivatives market it is the date of the next session.
756    #[serde(default)]
757    pub trade_date: Option<String>,
758    /// Order number.
759    #[serde(default)]
760    pub order_number: Option<String>,
761    /// Accrued coupon income.
762    #[serde(default)]
763    pub accrued_coupon: Option<Decimal>,
764    /// Volume of the trade.
765    #[serde(default)]
766    pub execution_value: Option<Decimal>,
767    /// Commission.
768    #[serde(default)]
769    pub commission: Option<Decimal>,
770    /// Exchange id.
771    #[serde(default)]
772    pub security_exchange: Option<String>,
773}
774
775// ─── portfolio ────────────────────────────────────────────────────────────────
776
777/// A portfolio frame: an array of positions, the same records as the REST portfolio.
778pub type WsPortfolioData = Vec<WsPortfolioPosition>;
779
780/// One position or balance of a portfolio frame.
781///
782/// The same fields as [`crate::http::models::PortfolioPosition`], without the deprecated ones the docs
783/// mark (sub-account, agreement, logo). All but `type` are optional.
784///
785/// # Examples
786///
787/// ```
788/// # use rust_decimal::Decimal;
789/// # use rust_decimal_macros::dec;
790/// use brokapi_bcs_protocol::ws::models::WsPortfolioPosition;
791///
792/// let p: WsPortfolioPosition = serde_json::from_str(
793///     r#"{"type":"depoLimit","ticker":"SBER","quantity":10,"unrealizedPL":40.0}"#,
794/// )?;
795/// assert_eq!(p.unrealized_pl, Some(dec!(40.0)));
796/// # Ok::<(), serde_json::Error>(())
797/// ```
798#[derive(Debug, Clone, Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct WsPortfolioPosition {
801    /// Kind of the record (JSON field `type`), for example `depoLimit` for a securities position and `moneyLimit` for a currency balance.
802    #[serde(rename = "type")]
803    pub position_type: String,
804    /// Ticker.
805    #[serde(default)]
806    pub ticker: Option<String>,
807    /// Name of the instrument for display.
808    #[serde(default)]
809    pub display_name: Option<String>,
810    /// Ticker of the base asset, for derivatives.
811    #[serde(default)]
812    pub base_asset_ticker: Option<String>,
813    /// Price currency.
814    #[serde(default)]
815    pub currency: Option<String>,
816    /// Exchange code.
817    #[serde(default)]
818    pub exchange: Option<String>,
819    /// Trading account.
820    #[serde(default)]
821    pub account: Option<String>,
822    /// Top-level instrument type, as text. The broker docs list no values.
823    #[serde(default)]
824    pub upper_type: Option<String>,
825    /// Instrument type, as text.
826    #[serde(default)]
827    pub instrument_type: Option<String>,
828    /// Settlement term code.
829    #[serde(default)]
830    pub term: Option<String>,
831    /// Quantity in pieces.
832    #[serde(default)]
833    pub quantity: Option<Decimal>,
834    /// Quantity held by active orders or by collateral (initial margin).
835    #[serde(default)]
836    pub locked: Option<Decimal>,
837    /// Balance price (the price the position was opened at), in the price currency.
838    #[serde(default)]
839    pub balance_price: Option<Decimal>,
840    /// Current price.
841    #[serde(default)]
842    pub current_price: Option<Decimal>,
843    /// Balance value (at the opening price), in the price currency.
844    #[serde(default)]
845    pub balance_value: Option<Decimal>,
846    /// Balance value in roubles.
847    #[serde(default)]
848    pub balance_value_rub: Option<Decimal>,
849    /// Balance value in dollars.
850    #[serde(default)]
851    pub balance_value_usd: Option<Decimal>,
852    /// Balance value in euros.
853    #[serde(default)]
854    pub balance_value_eur: Option<Decimal>,
855    /// Current value in the price currency.
856    #[serde(default)]
857    pub current_value: Option<Decimal>,
858    /// Current value in roubles.
859    #[serde(default)]
860    pub current_value_rub: Option<Decimal>,
861    /// Current value in dollars.
862    #[serde(default)]
863    pub current_value_usd: Option<Decimal>,
864    /// Current value in euros.
865    #[serde(default)]
866    pub current_value_eur: Option<Decimal>,
867    /// Price change since the position was opened (JSON field `unrealizedPL`, with capital letters).
868    #[serde(default, rename = "unrealizedPL", alias = "unrealizedPl")]
869    pub unrealized_pl: Option<Decimal>,
870    /// The same change in percent (JSON field `unrealizedPercentPL`).
871    #[serde(default, rename = "unrealizedPercentPL", alias = "unrealizedPercentPl")]
872    pub unrealized_percent_pl: Option<f64>,
873    /// Price change over the day (JSON field `dailyPL`). The broker docs word it as "daily change of the price since the position was opened".
874    #[serde(default, rename = "dailyPL", alias = "dailyPl")]
875    pub daily_pl: Option<Decimal>,
876    /// The same change in percent (JSON field `dailyPercentPL`).
877    #[serde(default, rename = "dailyPercentPL", alias = "dailyPercentPl")]
878    pub daily_percent_pl: Option<f64>,
879    /// Share of the portfolio in percent, of its value in roubles.
880    #[serde(default)]
881    pub portfolio_share: Option<f64>,
882    /// Number of decimal places of the price.
883    #[serde(default)]
884    pub scale: Option<u32>,
885    /// Minimum price step.
886    #[serde(default)]
887    pub minimum_step: Option<Decimal>,
888    /// Class code (board) of the instrument.
889    #[serde(default)]
890    pub board: Option<String>,
891    /// Unit of the price.
892    #[serde(default)]
893    pub price_unit: Option<String>,
894    /// Face value, for bonds.
895    #[serde(default)]
896    pub face_value: Option<Decimal>,
897    /// Accrued coupon income, for bonds.
898    #[serde(default)]
899    pub accrued_income: Option<Decimal>,
900    /// `true` if the class is blocked.
901    #[serde(default)]
902    pub is_blocked: Option<bool>,
903    /// `true` if the trading account is blocked.
904    #[serde(default)]
905    pub is_blocked_trade_account: Option<bool>,
906    /// Amount reserved as collateral (initial margin) on futures.
907    #[serde(default)]
908    pub locked_for_futures: Option<Decimal>,
909    /// Quantity in a lot.
910    #[serde(default)]
911    pub ratio_quantity: Option<Decimal>,
912    /// Expiration date, kept as text.
913    #[serde(default)]
914    pub expire_date: Option<String>,
915}
916
917// ─── limits ───────────────────────────────────────────────────────────────────
918
919/// A limits frame: one object with the current limits of the account.
920///
921/// The four lists are optional, because the broker may leave out a list that is empty.
922///
923/// # Examples
924///
925/// ```
926/// # use rust_decimal::Decimal;
927/// # use rust_decimal_macros::dec;
928/// use brokapi_bcs_protocol::ws::models::LimitsData;
929///
930/// let l: LimitsData = serde_json::from_str(
931///     r#"{"moneyLimits":[{"currencyCode":"RUB","locked":10.0,"quantity":{"type":"T0","value":500.0}}]}"#,
932/// )?;
933/// assert!(l.depo_limit.is_none());
934/// assert_eq!(l.money_limits.unwrap()[0].quantity.as_ref().unwrap().value, dec!(500.0));
935/// # Ok::<(), serde_json::Error>(())
936/// ```
937#[derive(Debug, Clone, Deserialize)]
938#[serde(rename_all = "camelCase")]
939pub struct LimitsData {
940    /// Securities positions.
941    #[serde(default)]
942    pub depo_limit: Option<Vec<DepoLimitEntry>>,
943    /// Futures positions.
944    #[serde(default)]
945    pub future_holding: Option<Vec<FutureHoldingEntry>>,
946    /// Money balances.
947    #[serde(default)]
948    pub money_limits: Option<Vec<MoneyLimitEntry>>,
949    /// Limits of the derivatives market.
950    #[serde(default)]
951    pub futures_limits: Option<Vec<FuturesLimitEntry>>,
952}
953
954/// A securities position in a limits frame (`depoLimit`).
955#[derive(Debug, Clone, Deserialize)]
956#[serde(rename_all = "camelCase")]
957pub struct DepoLimitEntry {
958    /// Ticker.
959    #[serde(default)]
960    pub ticker: Option<String>,
961    /// Class code (board).
962    #[serde(default)]
963    pub class_code: Option<String>,
964    /// Exchange code.
965    #[serde(default)]
966    pub exchange: Option<String>,
967    /// Weighted average price at which the position was opened.
968    #[serde(default)]
969    pub average_price: Option<Decimal>,
970    /// Quantity in pieces, with the settlement mode.
971    #[serde(default)]
972    pub quantity: Option<Quantity>,
973    /// Quantity in lots, with the settlement mode.
974    #[serde(default)]
975    pub quantity_batch: Option<Quantity>,
976    /// Instrument type as the broker names it.
977    #[serde(default)]
978    pub instrument_type: Option<String>,
979    /// Time the broker updated the data, kept as text.
980    #[serde(default)]
981    pub load_date: Option<String>,
982    /// Money held by active buy orders for this security.
983    #[serde(default)]
984    pub locked_buy_value: Option<Decimal>,
985    /// Money held by active sell orders for this security. The docs word it the same way as for buy.
986    #[serde(default)]
987    pub locked_sell_value: Option<Decimal>,
988}
989
990/// A money balance in a limits frame (`moneyLimits`).
991#[derive(Debug, Clone, Deserialize)]
992#[serde(rename_all = "camelCase")]
993pub struct MoneyLimitEntry {
994    /// Exchange code.
995    #[serde(default)]
996    pub exchange: Option<String>,
997    /// Currency code (JSON field `currencyCode`).
998    #[serde(default, rename = "currencyCode")]
999    pub currency: Option<String>,
1000    /// Money held by active orders.
1001    #[serde(default)]
1002    pub locked: Option<Decimal>,
1003    /// Weighted average opening price.
1004    #[serde(default)]
1005    pub average_price: Option<Decimal>,
1006    /// Instrument type as the broker names it.
1007    #[serde(default)]
1008    pub instrument_type: Option<String>,
1009    /// Amount of money, with the settlement mode.
1010    #[serde(default)]
1011    pub quantity: Option<Quantity>,
1012    /// Time the broker updated the data, kept as text.
1013    #[serde(default)]
1014    pub load_date: Option<String>,
1015}
1016
1017/// A futures position in a limits frame (`futureHolding`).
1018#[derive(Debug, Clone, Deserialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct FutureHoldingEntry {
1021    /// Ticker.
1022    #[serde(default)]
1023    pub ticker: Option<String>,
1024    /// Class code (board).
1025    #[serde(default)]
1026    pub class_code: Option<String>,
1027    /// Exchange code.
1028    #[serde(default)]
1029    pub exchange: Option<String>,
1030    /// Estimated value of the planned positions after the nearest clearing.
1031    #[serde(default)]
1032    pub cbpl_planned: Option<Decimal>,
1033    /// Estimate of the variation margin on the positions.
1034    #[serde(default)]
1035    pub var_margin: Option<Decimal>,
1036    /// Value of the derivatives market positions.
1037    #[serde(default)]
1038    pub position_value: Option<Decimal>,
1039    /// Current net positions.
1040    #[serde(default)]
1041    pub total_net: Option<Decimal>,
1042    /// Execution date, kept as text.
1043    #[serde(default)]
1044    pub execution_date: Option<String>,
1045    /// Total variation margin of the main clearing, credited on all positions.
1046    #[serde(default)]
1047    pub total_var_margin: Option<Decimal>,
1048    /// Variation margin credited during clearing.
1049    #[serde(default)]
1050    pub real_var_margin: Option<Decimal>,
1051    /// Weighted average opening price.
1052    #[serde(default)]
1053    pub average_price: Option<Decimal>,
1054    /// Instrument type as the broker names it.
1055    #[serde(default)]
1056    pub instrument_type: Option<String>,
1057    /// Date of the trading session, kept as text.
1058    #[serde(default)]
1059    pub trade_date: Option<String>,
1060}
1061
1062/// A limit of the derivatives market in a limits frame (`futuresLimits`).
1063#[derive(Debug, Clone, Deserialize)]
1064#[serde(rename_all = "camelCase")]
1065pub struct FuturesLimitEntry {
1066    /// Currency code (JSON field `currencyCode`).
1067    #[serde(default, rename = "currencyCode")]
1068    pub currency: Option<String>,
1069    /// Exchange code.
1070    #[serde(default)]
1071    pub exchange: Option<String>,
1072    /// Variation margin for the current day (the JSON field is spelled `accruedint`).
1073    #[serde(default, rename = "accruedint")]
1074    pub accrued_int: Option<Decimal>,
1075    /// Current limit of open positions.
1076    #[serde(default)]
1077    pub cbp_limit: Option<Decimal>,
1078    /// Positions after the last clearing.
1079    #[serde(default)]
1080    pub cbpl_used: Option<Decimal>,
1081    /// Estimated value of the planned positions after the nearest clearing.
1082    #[serde(default)]
1083    pub cbpl_planned: Option<Decimal>,
1084    /// Collateral (initial margin) reserved for the client orders.
1085    #[serde(default)]
1086    pub cbpl_used_for_orders: Option<Decimal>,
1087    /// Collateral (initial margin) reserved for the open client positions.
1088    #[serde(default)]
1089    pub cbpl_used_for_positions: Option<Decimal>,
1090    /// Premiums on options.
1091    #[serde(default)]
1092    pub options_premium: Option<Decimal>,
1093    /// Instrument type as the broker names it.
1094    #[serde(default)]
1095    pub instrument_type: Option<String>,
1096    /// Time the broker updated the data, kept as text.
1097    #[serde(default)]
1098    pub load_date: Option<String>,
1099    /// Estimate of the variation margin on the positions.
1100    #[serde(default)]
1101    pub var_margin: Option<Decimal>,
1102    /// Variation margin credited during clearing.
1103    #[serde(default)]
1104    pub real_var_margin: Option<Decimal>,
1105}
1106
1107/// An amount with its settlement mode (`T0`, `T1`, `T365` and so on), in limits frames.
1108#[derive(Debug, Clone, Deserialize)]
1109#[serde(rename_all = "camelCase")]
1110pub struct Quantity {
1111    /// Settlement mode, such as `T0` (JSON field `type`).
1112    #[serde(rename = "type")]
1113    pub quantity_type: String,
1114    /// The amount: pieces, lots or money, depending on the field that holds it.
1115    pub value: Decimal,
1116}
1117
1118// ─── marginal_indicators ──────────────────────────────────────────────────────
1119
1120/// A margin indicators frame: the margin picture of the account in one object.
1121///
1122/// The broker docs describe nine parts. Every part is optional here. **Some parts of this library do not
1123/// match the docs:** the docs name the fields of `marginParameters` `marginValue`, `collateral`,
1124/// `marginCall` and `forcedClosure`, and give `futuresParameters` as `futuresValue` and a
1125/// `futuresPortfolio` list. [`MarginParameters`] and [`FuturesParameters`] read other names, so they stay
1126/// `None` against a broker that follows the docs. The docs list `fortsMarginCall`, `fortsForcedClosure`
1127/// and `riskLevel` for the derivatives stability, which [`FortsStability`] does not read either. Not
1128/// verified against the live API.
1129///
1130/// # Examples
1131///
1132/// ```
1133/// # use rust_decimal::Decimal;
1134/// # use rust_decimal_macros::dec;
1135/// use brokapi_bcs_protocol::ws::models::MarginalIndicatorsData;
1136///
1137/// let m: MarginalIndicatorsData = serde_json::from_str(
1138///     r#"{"portfolioCurrentValue":{"currentValueRub":512000.0},"cashByCurrency":[{"currency":"RUB","sum":50000.0}]}"#,
1139/// )?;
1140/// assert_eq!(m.portfolio_current_value.unwrap().current_value_rub, Some(dec!(512000.0)));
1141/// assert_eq!(m.cash_by_currency.len(), 1);
1142/// # Ok::<(), serde_json::Error>(())
1143/// ```
1144#[derive(Debug, Clone, Deserialize)]
1145#[serde(rename_all = "camelCase")]
1146pub struct MarginalIndicatorsData {
1147    /// Data about the account.
1148    #[serde(default)]
1149    pub agreement_data: Option<AgreementData>,
1150    /// Current value of the portfolio and its changes.
1151    #[serde(default)]
1152    pub portfolio_current_value: Option<PortfolioCurrentValue>,
1153    /// Stability (margin level) of the portfolio.
1154    #[serde(default)]
1155    pub portfolio_stability: Option<PortfolioStability>,
1156    /// Stability of the derivatives market account.
1157    #[serde(default)]
1158    pub forts_stability: Option<FortsStability>,
1159    /// Parameters of the margin portfolio. See the note on the type.
1160    #[serde(default)]
1161    pub margin_parameters: Option<MarginParameters>,
1162    /// Free money by currency. Empty if the broker sent none.
1163    #[serde(default)]
1164    pub cash_by_currency: Vec<CashByCurrency>,
1165    /// Debts by currency.
1166    #[serde(default)]
1167    pub money_debt: Option<MoneyDebtWrapper>,
1168    /// Margin positions.
1169    #[serde(default)]
1170    pub margin_securities: Option<MarginSecuritiesWrapper>,
1171    /// Futures positions. See the note on the type.
1172    #[serde(default)]
1173    pub futures_parameters: Option<FuturesParameters>,
1174}
1175
1176/// Data about the brokerage account in a margin indicators frame (`agreementData`).
1177#[derive(Debug, Clone, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub struct AgreementData {
1180    /// Account number.
1181    #[serde(default)]
1182    pub number: Option<String>,
1183    /// `true` if the account is an IIA (individual investment account).
1184    #[serde(default)]
1185    pub is_iia: Option<bool>,
1186    /// Name for display.
1187    #[serde(default)]
1188    pub display_name: Option<String>,
1189    /// Id of the sub-account.
1190    #[serde(default)]
1191    pub subaccount_id: Option<String>,
1192    /// Colour of the account as a HEX code.
1193    #[serde(default)]
1194    pub color: Option<String>,
1195}
1196
1197/// Free money in one currency (`cashByCurrency`).
1198#[derive(Debug, Clone, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct CashByCurrency {
1201    /// Currency.
1202    #[serde(default)]
1203    pub currency: Option<String>,
1204    /// Amount.
1205    #[serde(default)]
1206    pub sum: Option<Decimal>,
1207}
1208
1209/// Debts of the account (`moneyDebt`): the total and the debt per currency.
1210#[derive(Debug, Clone, Deserialize)]
1211#[serde(rename_all = "camelCase")]
1212pub struct MoneyDebtWrapper {
1213    /// Total debt.
1214    #[serde(default)]
1215    pub total_money_debt: Option<Decimal>,
1216    /// Debt per currency.
1217    #[serde(default)]
1218    pub debt_by_currency: Vec<DebtByCurrency>,
1219}
1220
1221/// Debt in one currency.
1222#[derive(Debug, Clone, Deserialize)]
1223#[serde(rename_all = "camelCase")]
1224pub struct DebtByCurrency {
1225    /// Currency.
1226    #[serde(default)]
1227    pub currency: Option<String>,
1228    /// Amount of the debt.
1229    #[serde(default)]
1230    pub sum: Option<Decimal>,
1231}
1232
1233/// Margin positions (`marginSecurities`): the total and the list.
1234#[derive(Debug, Clone, Deserialize)]
1235#[serde(rename_all = "camelCase")]
1236pub struct MarginSecuritiesWrapper {
1237    /// Total. The docs label this field "total debt".
1238    #[serde(default)]
1239    pub total_margin_security: Option<Decimal>,
1240    /// The margin positions.
1241    #[serde(default)]
1242    pub margin_by_security: Vec<MarginBySecurity>,
1243}
1244
1245/// One margin position.
1246#[derive(Debug, Clone, Deserialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct MarginBySecurity {
1249    /// Ticker.
1250    #[serde(default)]
1251    pub ticker: Option<String>,
1252    /// Trading mode (board).
1253    #[serde(default)]
1254    pub board: Option<String>,
1255    /// Name for display.
1256    #[serde(default)]
1257    pub display_name: Option<String>,
1258    /// Link to a logo. Deprecated by the broker.
1259    #[serde(default)]
1260    pub logo_link: Option<String>,
1261    /// Quantity.
1262    #[serde(default)]
1263    pub quantity: Option<Decimal>,
1264    /// Current value.
1265    #[serde(default)]
1266    pub current_value: Option<Decimal>,
1267    /// Currency.
1268    #[serde(default)]
1269    pub currency: Option<String>,
1270    /// Discount in percent.
1271    #[serde(default)]
1272    pub discount: Option<f64>,
1273}
1274
1275/// Current value of the portfolio and its changes (`portfolioCurrentValue`).
1276#[derive(Debug, Clone, Deserialize)]
1277#[serde(rename_all = "camelCase")]
1278pub struct PortfolioCurrentValue {
1279    /// Current value in roubles.
1280    #[serde(default)]
1281    pub current_value_rub: Option<Decimal>,
1282    /// Change over the day, in roubles.
1283    #[serde(default)]
1284    pub daily_portfolio_change_rub: Option<Decimal>,
1285    /// Change over the day, in percent.
1286    #[serde(default)]
1287    pub daily_portfolio_change_perc: Option<f64>,
1288    /// Change over all time, in roubles.
1289    #[serde(default)]
1290    pub total_portfolio_change_rub: Option<Decimal>,
1291    /// Change over all time, in percent.
1292    #[serde(default)]
1293    pub total_portfolio_change_perc: Option<f64>,
1294    /// Collateral available.
1295    #[serde(default)]
1296    pub collateral_available: Option<Decimal>,
1297    /// Variation margin.
1298    #[serde(default)]
1299    pub var_margin: Option<Decimal>,
1300}
1301
1302/// Stability of the portfolio (`portfolioStability`).
1303#[derive(Debug, Clone, Deserialize)]
1304#[serde(rename_all = "camelCase")]
1305pub struct PortfolioStability {
1306    /// `true` if margin trading is on.
1307    #[serde(default)]
1308    pub is_margin_on: Option<bool>,
1309    /// All debts, in roubles.
1310    #[serde(default)]
1311    pub borrowed_funds: Option<Decimal>,
1312    /// Id of the status, 0 to 5 by the broker docs.
1313    #[serde(default)]
1314    pub stability_status_id: Option<u32>,
1315    /// Text of the status.
1316    #[serde(default)]
1317    pub stability_status_name: Option<String>,
1318    /// Amount needed to get out of a margin call.
1319    #[serde(default)]
1320    pub refill_sum: Option<Decimal>,
1321}
1322
1323/// Stability of the derivatives market account (`fortsStability`).
1324#[derive(Debug, Clone, Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326pub struct FortsStability {
1327    /// Own funds.
1328    #[serde(default)]
1329    pub own_funds: Option<Decimal>,
1330    /// Free funds.
1331    #[serde(default)]
1332    pub free_cash: Option<Decimal>,
1333    /// Id of the status.
1334    #[serde(default)]
1335    pub stability_status_id: Option<u32>,
1336    /// Text of the status.
1337    #[serde(default)]
1338    pub stability_status_name: Option<String>,
1339}
1340
1341/// Parameters of the margin portfolio (`marginParameters`).
1342///
1343/// The fields here are the names this library reads. The current docs name the fields `marginValue`,
1344/// `collateral`, `marginCall` and `forcedClosure` instead. See the note on [`MarginalIndicatorsData`].
1345#[derive(Debug, Clone, Deserialize)]
1346#[serde(rename_all = "camelCase")]
1347pub struct MarginParameters {
1348    /// Portfolio value.
1349    #[serde(default)]
1350    pub portfolio_value: Option<Decimal>,
1351    /// Initial margin (collateral).
1352    #[serde(default)]
1353    pub initial_margin: Option<Decimal>,
1354    /// Minimal margin.
1355    #[serde(default)]
1356    pub minimal_margin: Option<Decimal>,
1357    /// Corrected margin.
1358    #[serde(default)]
1359    pub corrected_margin: Option<Decimal>,
1360    /// Corrected portfolio value.
1361    #[serde(default)]
1362    pub corrected_portfolio_value: Option<Decimal>,
1363    /// Liquid portfolio value.
1364    #[serde(default)]
1365    pub liquid_portfolio_value: Option<Decimal>,
1366    /// Assets without a discount.
1367    #[serde(default)]
1368    pub assets_without_discount: Option<Decimal>,
1369}
1370
1371/// Futures parameters (`futuresParameters`).
1372///
1373/// The fields here are the names this library reads. The current docs give `futuresValue` and a
1374/// `futuresPortfolio` list instead. See the note on [`MarginalIndicatorsData`].
1375#[derive(Debug, Clone, Deserialize)]
1376#[serde(rename_all = "camelCase")]
1377pub struct FuturesParameters {
1378    /// Initial margin (collateral).
1379    #[serde(default)]
1380    pub initial_margin: Option<Decimal>,
1381    /// Minimal margin.
1382    #[serde(default)]
1383    pub minimal_margin: Option<Decimal>,
1384    /// Variation margin.
1385    #[serde(default)]
1386    pub var_margin: Option<Decimal>,
1387    /// Free funds.
1388    #[serde(default)]
1389    pub free_cash: Option<Decimal>,
1390}