Skip to main content

brokapi_bcs/map/
instrument.rs

1//! Instrument reference data mapping.
2
3use brokapi_bcs_protocol::http::models::{
4    InstrumentInfo, TradingParamsSnapshot, TradingStatusResponse,
5};
6use brokapi_core::{
7    Instrument, InstrumentId, InstrumentKey, InstrumentKind, Money, Price, Qty,
8    TradingAvailability, TradingParams, TradingStatus,
9};
10
11use super::instrument_id_ticker;
12
13/// Maps trading parameters of one board.
14///
15/// `min_qty` is the lot size, because BCS gives no separate minimum. Prices and steps are passed
16/// through unchanged.
17pub fn map_trading_params(snapshot: &TradingParamsSnapshot) -> TradingParams {
18    let instrument = instrument_id_ticker(&snapshot.ticker, &snapshot.class_code);
19    TradingParams {
20        instrument,
21        lot_size: Qty::new(snapshot.lot_size),
22        min_qty: Qty::new(snapshot.lot_size),
23        price_step: Price::new(snapshot.price_step),
24        price_step_value: Money::new(snapshot.price_step_value),
25        currency: snapshot.currency.clone(),
26    }
27}
28
29/// Maps the directory record of an instrument, for the board `class_code`.
30///
31/// - Name: `displayName`, else `shortName`, else the ticker.
32/// - Kind: from the BCS type text (contains `STOCK`, `BOND`, `FUTURE`, `OPTION`, `ETF`, `FUND`,
33///   `CURRENCY`), anything else is `Other`.
34/// - Currency: the trading currency, `RUB` when the record has none (an assumption).
35/// - Exchange: the exchange of the board with this class code, `MOEX` when the board is not listed
36///   (an assumption).
37pub fn map_instrument_info(info: &InstrumentInfo, class_code: &str) -> Instrument {
38    let instrument = instrument_id_ticker(&info.ticker, class_code);
39    Instrument {
40        id: instrument,
41        name: info
42            .display_name
43            .clone()
44            .or_else(|| info.short_name.clone())
45            .unwrap_or_else(|| info.ticker.clone()),
46        isin: info.isin.clone(),
47        kind: map_kind(info.instrument_type.as_deref()),
48        currency: info
49            .trading_currency
50            .clone()
51            .unwrap_or_else(|| "RUB".into()),
52        exchange: info
53            .boards
54            .iter()
55            .find(|b| b.class_code == class_code)
56            .map(|b| b.exchange.clone())
57            .unwrap_or_else(|| "MOEX".into()),
58    }
59}
60
61/// Maps the trading status of a board.
62///
63/// `OPEN` is [`TradingAvailability::Open`], `CLOSE` is `Closed` (any letter case), any other text is
64/// `Unknown`. The `reason` is the session type text from the broker. The status carries no timestamp.
65///
66/// # Examples
67///
68/// ```
69/// use brokapi_bcs_protocol::http::models::TradingStatusResponse;
70/// use brokapi_bcs::map::instrument::map_trading_status;
71/// use brokapi_core::TradingAvailability;
72///
73/// let resp: TradingStatusResponse = serde_json::from_str(
74///     r#"{"tradingSessionTypeId":1,"tradingSessionType":"Main","tradingSessionStatus":"OPEN",
75///         "nextSessionDate":"2026-10-20T07:00:00Z"}"#,
76/// )
77/// .unwrap();
78/// let status = map_trading_status("SBER", "TQBR", &resp);
79/// assert_eq!(status.availability, TradingAvailability::Open);
80/// ```
81pub fn map_trading_status(
82    ticker: &str,
83    class_code: &str,
84    resp: &TradingStatusResponse,
85) -> TradingStatus {
86    let instrument = instrument_id_ticker(ticker, class_code);
87    let availability = if resp.trading_session_status.eq_ignore_ascii_case("OPEN") {
88        TradingAvailability::Open
89    } else if resp.trading_session_status.eq_ignore_ascii_case("CLOSE") {
90        TradingAvailability::Closed
91    } else {
92        TradingAvailability::Unknown
93    };
94    TradingStatus {
95        instrument,
96        availability,
97        reason: Some(resp.trading_session_type.clone()),
98        ts: None,
99    }
100}
101
102fn map_kind(raw: Option<&str>) -> InstrumentKind {
103    match raw {
104        Some(s) if s.contains("STOCK") => InstrumentKind::Stock,
105        Some(s) if s.contains("BOND") => InstrumentKind::Bond,
106        Some(s) if s.contains("FUTURE") => InstrumentKind::Future,
107        Some(s) if s.contains("OPTION") => InstrumentKind::Option,
108        Some(s) if s.contains("ETF") => InstrumentKind::Etf,
109        Some(s) if s.contains("FUND") => InstrumentKind::MutualFund,
110        Some(s) if s.contains("CURRENCY") => InstrumentKind::Currency,
111        _ => InstrumentKind::Other,
112    }
113}
114
115/// Returns the broker-neutral key of an [`InstrumentId`] (a clone of `id.key`).
116pub fn instrument_from_native(id: &InstrumentId) -> InstrumentKey {
117    id.key.clone()
118}