brokapi_finam/map/
instrument.rs1use brokapi_core::{
4 Instrument, InstrumentKind, Money, Price, Qty, Schedule, SessionInterval, SessionKind,
5 TradingAvailability, TradingParams, UnixNanos,
6};
7use brokapi_finam_protocol::pb::grpc::tradeapi::v1::assets as pb;
8use std::time::Instant;
9
10use crate::util::{instrument_from_symbol, qty, timestamps, ts_nanos};
11
12pub fn kind_from_type(t: &str) -> InstrumentKind {
14 let t = t.to_ascii_uppercase();
15 let has = |s: &str| t.contains(s);
16 if has("OPTION") {
17 InstrumentKind::Option
18 } else if has("FUTURE") {
19 InstrumentKind::Future
20 } else if has("BOND") {
21 InstrumentKind::Bond
22 } else if has("ETF") || has("FUND") {
23 InstrumentKind::Etf
24 } else if has("EQUIT") || has("STOCK") || has("SHARE") {
25 InstrumentKind::Stock
26 } else if has("CURRENC") {
27 InstrumentKind::Currency
28 } else {
29 InstrumentKind::Other
30 }
31}
32
33pub fn map_asset(a: &pb::Asset) -> Instrument {
37 let symbol = if a.symbol.is_empty() {
38 format!("{}@{}", a.ticker, a.mic)
39 } else {
40 a.symbol.clone()
41 };
42 Instrument {
43 id: instrument_from_symbol(&symbol),
44 name: a.name.clone(),
45 isin: (!a.isin.is_empty()).then(|| a.isin.clone()),
46 kind: kind_from_type(&a.r#type),
47 currency: String::new(),
49 exchange: a.mic.clone(),
50 }
51}
52
53pub fn map_get_asset(symbol: &str, a: &pb::GetAssetResponse) -> Instrument {
55 let id = instrument_from_symbol(symbol);
56 Instrument {
57 exchange: if a.mic.is_empty() {
58 id.key.mic.clone()
59 } else {
60 a.mic.clone()
61 },
62 id,
63 name: a.name.clone(),
64 isin: (!a.isin.is_empty()).then(|| a.isin.clone()),
65 kind: kind_from_type(&a.r#type),
66 currency: a.quote_currency.clone(),
67 }
68}
69
70pub fn map_trading_params(symbol: &str, a: &pb::GetAssetResponse) -> TradingParams {
74 let places = u32::try_from(a.decimals.clamp(0, 28)).unwrap_or(0);
76 let step = Price::from_scaled(a.min_step.max(1), places);
77 let lot = qty(&a.lot_size)
78 .filter(|l| l.is_positive())
79 .unwrap_or(Qty::from(1));
80 TradingParams {
81 instrument: instrument_from_symbol(symbol),
82 lot_size: lot,
83 min_qty: lot,
84 price_step: step,
85 price_step_value: Money::new(step.decimal()),
86 currency: a.quote_currency.clone(),
87 }
88}
89
90pub fn apply_trade_lot_size(params: &mut TradingParams, trade_lot_size: i64) {
93 if trade_lot_size > 0 {
94 let lot = Qty::from(trade_lot_size);
95 params.lot_size = lot;
96 params.min_qty = lot;
97 }
98}
99
100pub fn session_kind(t: &str) -> SessionKind {
102 let t = t.to_ascii_uppercase();
103 if t.contains("AUCTION") {
104 SessionKind::Auction
105 } else if t.contains("EARLY") || t.contains("PRE") || t.contains("MORNING") {
106 SessionKind::PreMarket
107 } else if t.contains("LATE")
108 || t.contains("POST")
109 || t.contains("EVENING")
110 || t.contains("AFTER")
111 {
112 SessionKind::PostMarket
113 } else if t.contains("CORE")
114 || t.contains("MAIN")
115 || t.contains("NORMAL")
116 || t.contains("REGULAR")
117 {
118 SessionKind::Main
119 } else {
120 SessionKind::Other
121 }
122}
123
124fn is_closed_type(t: &str) -> bool {
125 t.to_ascii_uppercase().contains("CLOSED")
126}
127
128pub fn map_schedule(
133 symbol: &str,
134 resp: &pb::ScheduleResponse,
135 from: UnixNanos,
136 to: UnixNanos,
137 now: Instant,
138) -> Schedule {
139 let sessions = resp
140 .sessions
141 .iter()
142 .filter(|s| !is_closed_type(&s.r#type))
143 .filter_map(|s| {
144 let iv = s.interval.as_ref()?;
145 let start = iv.start_time.as_ref().map(ts_nanos)?;
146 let end = iv.end_time.as_ref().map(ts_nanos)?;
147 (end > from && start < to).then(|| SessionInterval {
149 start,
150 end,
151 kind: session_kind(&s.r#type),
152 })
153 })
154 .collect();
155 Schedule {
156 exchange: symbol.to_string(),
157 sessions,
158 ts: timestamps(None, now),
159 }
160}
161
162pub fn availability_at(
164 resp: &pb::ScheduleResponse,
165 at: UnixNanos,
166) -> (TradingAvailability, Option<String>) {
167 for s in &resp.sessions {
168 let Some(iv) = &s.interval else { continue };
169 let (Some(a), Some(b)) = (iv.start_time.as_ref(), iv.end_time.as_ref()) else {
170 continue;
171 };
172 if ts_nanos(a) <= at && at < ts_nanos(b) {
173 return if is_closed_type(&s.r#type) {
174 (TradingAvailability::Closed, Some(s.r#type.clone()))
175 } else {
176 (TradingAvailability::Open, Some(s.r#type.clone()))
177 };
178 }
179 }
180 if resp.sessions.is_empty() {
181 (TradingAvailability::Unknown, Some("empty schedule".into()))
182 } else {
183 (
184 TradingAvailability::Closed,
185 Some("outside of scheduled sessions".into()),
186 )
187 }
188}