Skip to main content

brokapi_tbank/map/
instrument.rs

1//! Instrument directory and trading status.
2
3use brokapi_core::{
4    Instrument, InstrumentId, InstrumentKind, Money, Price, Qty, TradingAvailability,
5    TradingParams, TradingStatus,
6};
7use brokapi_tbank_protocol::v1 as pb;
8
9use crate::ids::{id_from_parts, price_of_quotation, wall_nanos};
10
11/// Instrument kind of a broker message to the domain [`InstrumentKind`].
12///
13/// Bond, share, currency, ETF, futures and option map one to one. Everything else (indicatives,
14/// structured notes, ...) is [`InstrumentKind::Other`].
15///
16/// # Examples
17///
18/// ```
19/// use brokapi_core::InstrumentKind;
20/// use brokapi_tbank::map::instrument::kind;
21/// use brokapi_tbank_protocol::v1::InstrumentType;
22///
23/// assert_eq!(kind(InstrumentType::Share), InstrumentKind::Stock);
24/// assert_eq!(kind(InstrumentType::Futures), InstrumentKind::Future);
25/// ```
26pub fn kind(k: pb::InstrumentType) -> InstrumentKind {
27    match k {
28        pb::InstrumentType::Bond => InstrumentKind::Bond,
29        pb::InstrumentType::Share => InstrumentKind::Stock,
30        pb::InstrumentType::Currency => InstrumentKind::Currency,
31        pb::InstrumentType::Etf => InstrumentKind::Etf,
32        pb::InstrumentType::Futures => InstrumentKind::Future,
33        pb::InstrumentType::Option => InstrumentKind::Option,
34        _ => InstrumentKind::Other,
35    }
36}
37
38/// Full record of `GetInstrumentBy` to [`Instrument`].
39///
40/// `exchange` holds the class code (the board), for example `TQBR`. `isin` is `None` when the broker
41/// sends none.
42///
43/// # Examples
44///
45/// ```
46/// use brokapi_tbank::map::instrument::instrument_from_full;
47/// use brokapi_tbank_protocol::v1::Instrument;
48///
49/// let record = Instrument { uid: "uid-sber".into(), ticker: "SBER".into(), class_code: "TQBR".into(), currency: "rub".into(), ..Default::default() };
50/// let instrument = instrument_from_full(&record);
51/// assert_eq!(instrument.id.key.symbol, "SBER");
52/// assert_eq!(instrument.currency, "rub");
53/// assert_eq!(instrument.isin, None);
54/// ```
55pub fn instrument_from_full(i: &pb::Instrument) -> Instrument {
56    Instrument {
57        id: id_from_parts(&i.uid, &i.figi, &i.ticker, &i.class_code),
58        name: i.name.clone(),
59        isin: (!i.isin.is_empty()).then(|| i.isin.clone()),
60        kind: kind(i.instrument_kind()),
61        currency: i.currency.clone(),
62        exchange: i.class_code.clone(),
63    }
64}
65
66/// Record of a whole-kind listing (`Shares`, `Bonds`, ...): the kind comes from the method, not the record.
67/// Futures and options have no ISIN.
68#[allow(clippy::too_many_arguments)]
69fn listed(
70    kind: InstrumentKind,
71    uid: &str,
72    figi: &str,
73    ticker: &str,
74    class_code: &str,
75    isin: &str,
76    name: &str,
77    currency: &str,
78) -> Instrument {
79    Instrument {
80        id: id_from_parts(uid, figi, ticker, class_code),
81        name: name.to_string(),
82        isin: (!isin.is_empty()).then(|| isin.to_string()),
83        kind,
84        currency: currency.to_string(),
85        exchange: class_code.to_string(),
86    }
87}
88
89/// A share record of the `Shares` listing to [`Instrument`] of kind `Stock`.
90pub fn from_share(i: &pb::Share) -> Instrument {
91    listed(
92        InstrumentKind::Stock,
93        &i.uid,
94        &i.figi,
95        &i.ticker,
96        &i.class_code,
97        &i.isin,
98        &i.name,
99        &i.currency,
100    )
101}
102
103/// A bond record of the `Bonds` listing to [`Instrument`] of kind `Bond`.
104pub fn from_bond(i: &pb::Bond) -> Instrument {
105    listed(
106        InstrumentKind::Bond,
107        &i.uid,
108        &i.figi,
109        &i.ticker,
110        &i.class_code,
111        &i.isin,
112        &i.name,
113        &i.currency,
114    )
115}
116
117/// An ETF record of the `Etfs` listing to [`Instrument`] of kind `Etf`.
118pub fn from_etf(i: &pb::Etf) -> Instrument {
119    listed(
120        InstrumentKind::Etf,
121        &i.uid,
122        &i.figi,
123        &i.ticker,
124        &i.class_code,
125        &i.isin,
126        &i.name,
127        &i.currency,
128    )
129}
130
131/// A futures record of the `Futures` listing to [`Instrument`] of kind `Future`. It has no ISIN.
132pub fn from_future(i: &pb::Future) -> Instrument {
133    listed(
134        InstrumentKind::Future,
135        &i.uid,
136        &i.figi,
137        &i.ticker,
138        &i.class_code,
139        "",
140        &i.name,
141        &i.currency,
142    )
143}
144
145/// An option record of the `Options` listing to [`Instrument`] of kind `Option`. It has no ISIN and no figi.
146pub fn from_option(i: &pb::Option) -> Instrument {
147    listed(
148        InstrumentKind::Option,
149        &i.uid,
150        "",
151        &i.ticker,
152        &i.class_code,
153        "",
154        &i.name,
155        &i.currency,
156    )
157}
158
159/// A currency record of the `Currencies` listing to [`Instrument`] of kind `Currency`.
160pub fn from_currency(i: &pb::Currency) -> Instrument {
161    listed(
162        InstrumentKind::Currency,
163        &i.uid,
164        &i.figi,
165        &i.ticker,
166        &i.class_code,
167        &i.isin,
168        &i.name,
169        &i.currency,
170    )
171}
172
173/// `FindInstrument` returns short records without currency; `currency` stays empty (use `get` for it).
174pub fn instrument_from_short(i: &pb::InstrumentShort) -> Instrument {
175    Instrument {
176        id: id_from_parts(&i.uid, &i.figi, &i.ticker, &i.class_code),
177        name: i.name.clone(),
178        isin: (!i.isin.is_empty()).then(|| i.isin.clone()),
179        kind: kind(i.instrument_kind()),
180        currency: String::new(),
181        exchange: i.class_code.clone(),
182    }
183}
184
185/// `lot_size` is units per lot. `min_qty` is one lot: an order is a whole number of lots (and the library
186/// counts the quantity of an order in units).
187/// `price_step_value` equals the step (price is per one instrument in the settlement currency);
188/// futures need `GetFuturesMargin` for the step in money, not requested here.
189pub fn trading_params(id: &InstrumentId, i: &pb::Instrument) -> TradingParams {
190    let step = i
191        .min_price_increment
192        .as_ref()
193        .map(price_of_quotation)
194        .unwrap_or(Price::ZERO);
195    TradingParams {
196        instrument: id.clone(),
197        lot_size: Qty::from(i.lot),
198        min_qty: Qty::from(i.lot),
199        price_step: step,
200        price_step_value: Money::new(step.decimal()),
201        currency: i.currency.clone(),
202    }
203}
204
205/// Broker trading status to [`TradingAvailability`].
206///
207/// Normal trading and open session are `Open`. A break is `Break`. Not available for trading and a
208/// closed session are `Closed`. Opening and closing periods and auctions are `Unknown`, because
209/// orders are possible there only with restrictions that this mapping does not model.
210///
211/// # Examples
212///
213/// ```
214/// use brokapi_core::TradingAvailability;
215/// use brokapi_tbank::map::instrument::availability;
216/// use brokapi_tbank_protocol::v1::SecurityTradingStatus;
217///
218/// assert_eq!(availability(SecurityTradingStatus::NormalTrading), TradingAvailability::Open);
219/// assert_eq!(availability(SecurityTradingStatus::BreakInTrading), TradingAvailability::Break);
220/// ```
221pub fn availability(s: pb::SecurityTradingStatus) -> TradingAvailability {
222    use pb::SecurityTradingStatus as S;
223    match s {
224        S::NormalTrading | S::SessionOpen | S::DealerNormalTrading => TradingAvailability::Open,
225        S::BreakInTrading | S::DealerBreakInTrading => TradingAvailability::Break,
226        S::NotAvailableForTrading
227        | S::SessionClose
228        | S::DealerNotAvailableForTrading
229        | S::SessionAssigned => TradingAvailability::Closed,
230        // Opening/closing periods and auctions: orders are possible with restrictions.
231        _ => TradingAvailability::Unknown,
232    }
233}
234
235/// Broker trading status to [`TradingStatus`] of `id`.
236///
237/// `reason` holds the broker's status name. `ts` is the local clock, since the status has no time of
238/// its own.
239///
240/// # Examples
241///
242/// ```
243/// use brokapi_core::{InstrumentId, InstrumentKey, NativeId, TradingAvailability};
244/// use brokapi_tbank::map::instrument::trading_status;
245/// use brokapi_tbank_protocol::v1::SecurityTradingStatus;
246///
247/// let id = InstrumentId::new(InstrumentKey::new("SBER", "TQBR"), NativeId::Uid("uid-sber".into()));
248/// let status = trading_status(&id, SecurityTradingStatus::SessionClose);
249/// assert_eq!(status.availability, TradingAvailability::Closed);
250/// ```
251pub fn trading_status(id: &InstrumentId, s: pb::SecurityTradingStatus) -> TradingStatus {
252    TradingStatus {
253        instrument: id.clone(),
254        availability: availability(s),
255        reason: Some(s.as_str_name().to_string()),
256        ts: Some(wall_nanos()),
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use brokapi_core::{Decimal, dec};
264    use brokapi_core::{InstrumentKey, NativeId};
265    use brokapi_tbank_protocol::money::quotation;
266
267    #[test]
268    fn full_instrument_mapping() {
269        let i = pb::Instrument {
270            uid: "u1".into(),
271            figi: "BBG".into(),
272            ticker: "SBER".into(),
273            class_code: "TQBR".into(),
274            isin: "RU0009029540".into(),
275            lot: 10,
276            currency: "rub".into(),
277            min_price_increment: Some(quotation("0.01")),
278            instrument_kind: pb::InstrumentType::Share as i32,
279            name: "Sberbank".into(),
280            ..Default::default()
281        };
282        let m = instrument_from_full(&i);
283        assert_eq!(m.id.key, InstrumentKey::new("SBER", "TQBR"));
284        assert_eq!(m.id.native, NativeId::Uid("u1".into()));
285        assert_eq!(m.kind, InstrumentKind::Stock);
286        let p = trading_params(&m.id, &i);
287        assert_eq!(
288            (
289                p.lot_size.decimal(),
290                p.price_step.decimal(),
291                p.min_qty.decimal()
292            ),
293            (dec!(10.0), dec!(0.01), dec!(10.0))
294        );
295    }
296
297    #[test]
298    fn trading_status_mapping() {
299        use pb::SecurityTradingStatus as S;
300        assert_eq!(availability(S::NormalTrading), TradingAvailability::Open);
301        assert_eq!(availability(S::BreakInTrading), TradingAvailability::Break);
302        assert_eq!(
303            availability(S::NotAvailableForTrading),
304            TradingAvailability::Closed
305        );
306        assert_eq!(
307            availability(S::ClosingAuction),
308            TradingAvailability::Unknown
309        );
310        assert_eq!(availability(S::Unspecified), TradingAvailability::Unknown);
311    }
312}