Skip to main content

brokapi_alor/map/
instrument.rs

1//! Reference data mapping: instruments, trading parameters, trading status.
2
3use brokapi_alor_protocol::http::models::SecurityInfo;
4use brokapi_core::{
5    Decimal, Instrument, InstrumentId, InstrumentKind, Money, Price, Qty, TradingAvailability,
6    TradingParams, TradingStatus,
7};
8
9use crate::util::instrument_id;
10
11/// The board an entry is about: the board of the entry, else the main one of the instrument.
12pub fn board_of(info: &SecurityInfo) -> String {
13    info.board
14        .clone()
15        .filter(|b| !b.is_empty())
16        .or_else(|| info.primary_board.clone().filter(|b| !b.is_empty()))
17        .unwrap_or_default()
18}
19
20/// The kind of an instrument, from its type, its market and the CFI code.
21pub fn kind_of(info: &SecurityInfo) -> InstrumentKind {
22    let ty = info.kind.as_deref().unwrap_or("");
23    match ty {
24        "CS" | "PS" | "RDR" => return InstrumentKind::Stock,
25        "EUSOV" | "MUNI" | "CORP" => return InstrumentKind::Bond,
26        "FOR" => return InstrumentKind::Currency,
27        "MF" => return InstrumentKind::MutualFund,
28        _ => {}
29    }
30    if ty.starts_with("Фьючерс") {
31        return InstrumentKind::Future;
32    }
33    if ty.starts_with("Марж") || ty.contains("Call") || ty.contains("Put") {
34        return InstrumentKind::Option;
35    }
36    match info.market.as_deref() {
37        Some("CURR") => return InstrumentKind::Currency,
38        Some("FORTS") => return InstrumentKind::Future,
39        _ => {}
40    }
41    match info.cfi_code.as_deref().and_then(|c| c.chars().next()) {
42        Some('E') => InstrumentKind::Stock,
43        Some('D') => InstrumentKind::Bond,
44        Some('F') => InstrumentKind::Future,
45        Some('O') => InstrumentKind::Option,
46        Some('C') => InstrumentKind::Etf,
47        _ => InstrumentKind::Other,
48    }
49}
50
51/// The id of the instrument of an entry.
52pub fn id_of(info: &SecurityInfo) -> InstrumentId {
53    instrument_id(&info.symbol, &board_of(info))
54}
55
56/// An instrument of the reference data.
57pub fn instrument(info: &SecurityInfo) -> Instrument {
58    Instrument {
59        id: id_of(info),
60        name: info
61            .shortname
62            .clone()
63            .or_else(|| info.description.clone())
64            .unwrap_or_else(|| info.symbol.clone()),
65        isin: info.isin.clone().filter(|i| !i.is_empty()),
66        kind: kind_of(info),
67        currency: info.currency.clone().unwrap_or_default(),
68        exchange: info.exchange.clone(),
69    }
70}
71
72/// The trading parameters of an instrument. The lot is in pieces; the minimal quantity is one lot. The value of a
73/// price step is the `pricestep` of the broker (the step in roubles), or the step itself when it gives none.
74pub fn trading_params(info: &SecurityInfo) -> TradingParams {
75    let lot = info
76        .lot_size
77        .filter(|l| !l.is_zero())
78        .unwrap_or(Decimal::ONE);
79    let step = info.min_step.unwrap_or(Decimal::new(1, 2));
80    TradingParams {
81        instrument: id_of(info),
82        lot_size: Qty::new(lot),
83        min_qty: Qty::new(lot),
84        price_step: Price::new(step),
85        price_step_value: Money::new(info.price_step.unwrap_or(step)),
86        currency: info.currency.clone().unwrap_or_default(),
87    }
88}
89
90/// The availability for a trading status code of the broker.
91pub fn availability_of(code: i32) -> TradingAvailability {
92    match code {
93        // 17 normal period, 118 opening period, 103 closing period, auctions (102, 106, 107, 119) and trading at the
94        // price of the closing auction (120): orders are accepted.
95        17 | 118 | 103 | 102 | 106 | 107 | 119 | 120 => TradingAvailability::Open,
96        2 => TradingAvailability::Break,
97        18 => TradingAvailability::Closed,
98        _ => TradingAvailability::Unknown,
99    }
100}
101
102/// The trading status of an instrument, from its `tradingStatus` code.
103pub fn trading_status(
104    info: &SecurityInfo,
105    now_ns: Option<brokapi_core::UnixNanos>,
106) -> TradingStatus {
107    TradingStatus {
108        instrument: id_of(info),
109        availability: info
110            .trading_status
111            .map(availability_of)
112            .unwrap_or(TradingAvailability::Unknown),
113        reason: info.trading_status_info.clone(),
114        ts: now_ns,
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn info(json: &str) -> SecurityInfo {
123        serde_json::from_str(json).unwrap()
124    }
125
126    #[test]
127    fn params_come_from_the_lot_and_the_step() {
128        let i = info(
129            r#"{"symbol":"SBER","exchange":"MOEX","board":"TQBR","type":"CS","lotsize":10,"minstep":0.01,"pricestep":0.01,"currency":"RUB"}"#,
130        );
131        let p = trading_params(&i);
132        assert_eq!(p.lot_size, Qty::from(10));
133        assert_eq!(p.min_qty, Qty::from(10));
134        assert_eq!(p.price_step.to_string(), "0.01");
135        assert_eq!(kind_of(&i), InstrumentKind::Stock);
136        assert_eq!(instrument(&i).id.key.display(), "SBER@TQBR");
137    }
138
139    #[test]
140    fn kinds_follow_the_type_the_market_and_the_cfi() {
141        let k = |j: &str| kind_of(&info(j));
142        assert_eq!(k(r#"{"type":"CORP"}"#), InstrumentKind::Bond);
143        assert_eq!(
144            k(r#"{"type":"Фьючерсный контракт SI"}"#),
145            InstrumentKind::Future
146        );
147        assert_eq!(
148            k(r#"{"type":"Марж. амер. Call SBER"}"#),
149            InstrumentKind::Option
150        );
151        assert_eq!(k(r#"{"market":"CURR"}"#), InstrumentKind::Currency);
152        assert_eq!(k(r#"{"cfiCode":"ESXXXX"}"#), InstrumentKind::Stock);
153        assert_eq!(k(r#"{}"#), InstrumentKind::Other);
154    }
155
156    #[test]
157    fn status_codes_become_availability() {
158        assert_eq!(availability_of(17), TradingAvailability::Open);
159        assert_eq!(availability_of(18), TradingAvailability::Closed);
160        assert_eq!(availability_of(2), TradingAvailability::Break);
161        assert_eq!(availability_of(999), TradingAvailability::Unknown);
162    }
163}