brokapi_alor/map/
instrument.rs1use 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
11pub 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
20pub 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
51pub fn id_of(info: &SecurityInfo) -> InstrumentId {
53 instrument_id(&info.symbol, &board_of(info))
54}
55
56pub 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
72pub 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
90pub fn availability_of(code: i32) -> TradingAvailability {
92 match code {
93 17 | 118 | 103 | 102 | 106 | 107 | 119 | 120 => TradingAvailability::Open,
96 2 => TradingAvailability::Break,
97 18 => TradingAvailability::Closed,
98 _ => TradingAvailability::Unknown,
99 }
100}
101
102pub 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}