1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use brokapi_alor_protocol::http::client::AlorHttpClient;
9use brokapi_alor_protocol::ws::models::WsSubscribe;
10use brokapi_alor_protocol::ws::session::WsSession;
11use brokapi_core::{
12 Candle, Envelope, Error, Instrument, InstrumentId, InstrumentKind, InstrumentQuery,
13 InstrumentSource, MarketDataCaps, MarketDataSource, MarketTrade, OrderBook, OrderBookSource,
14 Schedule, ShutdownMode, ShutdownReport, StreamingCaps, SubscribeOpts, SubscriptionId,
15 TimeRange, Timeframe, TradingParams, TradingStatus,
16};
17use brokapi_runtime::{BrokerLimiter, Emitter};
18use brokapi_sansio::SubscriptionKind;
19use tokio::sync::{Mutex, RwLock, mpsc};
20use tokio::task::JoinHandle;
21use tokio_util::sync::CancellationToken;
22
23use crate::config::MarketDataConfig;
24use crate::core::MdCore;
25use crate::error::{map_config_err, map_err};
26use crate::map::instrument::{instrument, trading_params, trading_status};
27use crate::map::market::{candles, market_trade, order_book};
28use crate::map::unix_nanos;
29use crate::pump::{drain_backlog, run_pump};
30use crate::raw::AlorRaw;
31use crate::subs::{MdSubKind, MdSubscription, SubIdGen, new_guid, wait_ack};
32use crate::util::{exchange_of_board, nanos_to_secs, ticker_board, timeframe_label};
33
34const MAX_DEPTH: u32 = 50;
36
37const KIND_PAGE_SIZE: u32 = 100;
39const MAX_KIND_PAGES: u32 = 200;
40
41struct MdShared {
42 http: AlorHttpClient,
43 ws: WsSession,
44 limiter: Arc<BrokerLimiter>,
45 core: Arc<Mutex<MdCore>>,
46 emitter: Arc<Emitter>,
47 subs: RwLock<HashMap<SubscriptionId, MdSubscription>>,
48 sub_ids: SubIdGen,
49 cancel: CancellationToken,
50 ack_timeout: Duration,
51}
52
53pub struct MarketDataConnector {
93 inner: Arc<MdShared>,
94 pump: JoinHandle<()>,
95}
96
97impl MarketDataConnector {
98 pub async fn start(
106 cfg: MarketDataConfig,
107 limiter: Arc<BrokerLimiter>,
108 out: mpsc::Sender<Envelope>,
109 ) -> Result<Self, Error> {
110 let http = AlorHttpClient::new(cfg.alor.clone()).map_err(map_config_err)?;
111 http.get_access_token().await.map_err(map_err)?;
112
113 let (event_tx, event_rx) = kanal::bounded_async(512);
114 let ws = WsSession::spawn_with(http.clone(), event_tx, cfg.ws_options());
115 let cancel = CancellationToken::new();
116 let core = Arc::new(Mutex::new(MdCore::new()));
117 let emitter = Emitter::new(Arc::clone(&cfg.source), out);
118
119 let inner = Arc::new(MdShared {
120 http,
121 ws,
122 limiter,
123 core: Arc::clone(&core),
124 emitter: Arc::clone(&emitter),
125 subs: RwLock::new(HashMap::new()),
126 sub_ids: SubIdGen::new(0),
127 cancel: cancel.clone(),
128 ack_timeout: cfg.subscribe_ack_timeout,
129 });
130 let pump = tokio::spawn(run_pump(
131 core,
132 emitter,
133 event_rx,
134 cancel,
135 |m| async move { m },
136 ));
137 Ok(Self { inner, pump })
138 }
139
140 pub fn raw(&self) -> AlorRaw {
142 AlorRaw::new(self.inner.http.clone(), Arc::clone(&self.inner.limiter))
143 }
144
145 pub fn caps(&self) -> MarketDataCaps {
147 MarketDataCaps::new(MAX_DEPTH, false, StreamingCaps::new(true, true, true, true))
148 }
149
150 pub async fn shutdown(self, mode: ShutdownMode) -> ShutdownReport {
153 let mut unsubscribed = 0usize;
154 let mut unsubscribe_failed = 0usize;
155 if !matches!(mode, ShutdownMode::Immediate) {
156 let subs: Vec<_> = self.inner.subs.read().await.values().cloned().collect();
157 for sub in subs {
158 match self.inner.ws.unsubscribe(&sub.guid).await {
159 Ok(()) => unsubscribed += 1,
160 Err(_) => unsubscribe_failed += 1,
161 }
162 }
163 }
164 if let ShutdownMode::Graceful { deadline } = mode {
165 drain_backlog(&self.inner.emitter, deadline).await;
166 }
167 self.inner.cancel.cancel();
168 let _ = self.pump.await;
169 ShutdownReport::new(unsubscribed, unsubscribe_failed, Vec::new())
170 }
171
172 fn acquire(&self, method: &str) -> Result<(), Error> {
173 self.inner.limiter.acquire(method, brokapi_runtime::now())
174 }
175
176 async fn register_md_sub(
177 &self,
178 instrument: InstrumentId,
179 kind: MdSubKind,
180 core_kind: SubscriptionKind,
181 opts: SubscribeOpts,
182 guid: String,
183 ws: WsSubscribe,
184 ) -> Result<SubscriptionId, Error> {
185 let id = self.inner.sub_ids.next();
186 self.inner
187 .emitter
188 .register(id, Some(instrument.clone()), opts.backpressure);
189 self.inner.core.lock().await.register(&guid, id, core_kind);
192 let notify = self.inner.core.lock().await.notifier();
193 if let Err(e) = self.inner.ws.subscribe(ws).await {
194 self.inner.core.lock().await.forget(&guid);
195 self.inner.emitter.unregister(id);
196 return Err(map_err(e));
197 }
198 let confirmed = wait_ack(
200 &self.inner.core,
201 notify,
202 self.inner.ack_timeout,
203 |c: &MdCore| {
204 (
205 c.actual_state(id).unwrap_or(crate::core::SubState::Pending),
206 c.failure(id).map(str::to_string),
207 )
208 },
209 )
210 .await;
211 if let Err(e) = confirmed {
212 let _ = self.inner.ws.unsubscribe(&guid).await;
213 self.inner.core.lock().await.forget(&guid);
214 self.inner.emitter.unregister(id);
215 return Err(e);
216 }
217 self.inner.subs.write().await.insert(
218 id,
219 MdSubscription {
220 id,
221 instrument,
222 kind,
223 opts,
224 guid,
225 },
226 );
227 Ok(id)
228 }
229}
230
231#[async_trait]
232impl brokapi_core::Connector for MarketDataConnector {
233 type Caps = brokapi_core::MarketDataCaps;
234
235 fn caps(&self) -> brokapi_core::MarketDataCaps {
236 MarketDataConnector::caps(self)
237 }
238
239 async fn shutdown(self, mode: brokapi_core::ShutdownMode) -> brokapi_core::ShutdownReport {
240 MarketDataConnector::shutdown(self, mode).await
241 }
242}
243
244#[async_trait]
245impl OrderBookSource for MarketDataConnector {
246 async fn order_book(&self, id: &InstrumentId, depth: u32) -> Result<OrderBook, Error> {
247 self.acquire("GetOrderBook")?;
248 let (ticker, board) = ticker_board(id)?;
249 let wire = self
250 .inner
251 .http
252 .get_order_book(
253 exchange_of_board(&board),
254 &ticker,
255 Some(&board),
256 Some(depth.clamp(1, MAX_DEPTH)),
257 )
258 .await
259 .map_err(map_err)?;
260 Ok(order_book(&wire, id, brokapi_runtime::now()))
261 }
262
263 async fn subscribe_order_book(
264 &self,
265 id: &InstrumentId,
266 opts: SubscribeOpts,
267 ) -> Result<SubscriptionId, Error> {
268 let depth = opts.depth.unwrap_or(MAX_DEPTH).clamp(1, MAX_DEPTH);
269 let (ticker, board) = ticker_board(id)?;
270 let guid = new_guid();
271 let ws = WsSubscribe::order_book(
272 &guid,
273 exchange_of_board(&board),
274 &ticker,
275 Some(&board),
276 depth,
277 );
278 self.register_md_sub(
279 id.clone(),
280 MdSubKind::OrderBook,
281 SubscriptionKind::OrderBook {
282 instrument: id.clone(),
283 depth,
284 },
285 opts,
286 guid,
287 ws,
288 )
289 .await
290 }
291
292 async fn unsubscribe(&self, sub: SubscriptionId) -> Result<(), Error> {
293 let record = self
294 .inner
295 .subs
296 .write()
297 .await
298 .remove(&sub)
299 .ok_or_else(|| Error::Rejected {
300 code: "unknown_subscription".into(),
301 message: format!("subscription {} is not active", sub.value()),
302 })?;
303 self.inner.core.lock().await.forget(&record.guid);
304 self.inner.emitter.unregister(sub);
305 self.inner
306 .ws
307 .unsubscribe(&record.guid)
308 .await
309 .map_err(map_err)
310 }
311}
312
313#[async_trait]
314impl MarketDataSource for MarketDataConnector {
315 async fn last_trades(&self, id: &InstrumentId, limit: u32) -> Result<Vec<MarketTrade>, Error> {
316 self.acquire("GetLastTrades")?;
317 let (ticker, board) = ticker_board(id)?;
318 let query = [
319 ("take", limit.to_string()),
320 ("descending", "true".to_string()),
321 ];
322 let records = self
323 .inner
324 .http
325 .get_all_trades(exchange_of_board(&board), &ticker, Some(&board), &query)
326 .await
327 .map_err(map_err)?;
328 let now = brokapi_runtime::now();
329 let mut trades: Vec<MarketTrade> = records
331 .iter()
332 .rev()
333 .filter_map(|r| market_trade(r, id, now))
334 .collect();
335 if trades.len() > limit as usize {
336 trades.drain(..trades.len() - limit as usize);
337 }
338 Ok(trades)
339 }
340
341 async fn subscribe_market_trades(
342 &self,
343 id: &InstrumentId,
344 opts: SubscribeOpts,
345 ) -> Result<SubscriptionId, Error> {
346 let (ticker, board) = ticker_board(id)?;
347 let guid = new_guid();
348 let ws = WsSubscribe::all_trades(&guid, exchange_of_board(&board), &ticker, Some(&board));
349 self.register_md_sub(
350 id.clone(),
351 MdSubKind::MarketTrades,
352 SubscriptionKind::LastTrades {
353 instrument: id.clone(),
354 },
355 opts,
356 guid,
357 ws,
358 )
359 .await
360 }
361
362 async fn subscribe_quotes(
363 &self,
364 id: &InstrumentId,
365 opts: SubscribeOpts,
366 ) -> Result<SubscriptionId, Error> {
367 let (ticker, board) = ticker_board(id)?;
368 let guid = new_guid();
369 let ws = WsSubscribe::quotes(&guid, exchange_of_board(&board), &ticker, Some(&board));
370 self.register_md_sub(
371 id.clone(),
372 MdSubKind::Quotes,
373 SubscriptionKind::Quotes {
374 instrument: id.clone(),
375 },
376 opts,
377 guid,
378 ws,
379 )
380 .await
381 }
382
383 async fn candles(
384 &self,
385 id: &InstrumentId,
386 tf: Timeframe,
387 range: TimeRange,
388 ) -> Result<Vec<Candle>, Error> {
389 self.acquire("GetCandles")?;
390 let (ticker, board) = ticker_board(id)?;
391 let resp = self
392 .inner
393 .http
394 .get_history(
395 exchange_of_board(&board),
396 &ticker,
397 Some(&board),
398 &timeframe_label(tf),
399 nanos_to_secs(range.from),
400 nanos_to_secs(range.to),
401 None,
402 )
403 .await
404 .map_err(map_err)?;
405 Ok(candles(&resp, id, tf, brokapi_runtime::now()))
406 }
407
408 async fn subscribe_candles(
409 &self,
410 id: &InstrumentId,
411 tf: Timeframe,
412 opts: SubscribeOpts,
413 ) -> Result<SubscriptionId, Error> {
414 let (ticker, board) = ticker_board(id)?;
415 let guid = new_guid();
416 let from = nanos_to_secs(unix_nanos(chrono::Utc::now()));
418 let ws = WsSubscribe::bars(
419 &guid,
420 exchange_of_board(&board),
421 &ticker,
422 Some(&board),
423 &timeframe_label(tf),
424 from,
425 true,
426 );
427 self.register_md_sub(
428 id.clone(),
429 MdSubKind::Candles,
430 SubscriptionKind::LastCandle {
431 instrument: id.clone(),
432 timeframe: tf,
433 },
434 opts,
435 guid,
436 ws,
437 )
438 .await
439 }
440}
441
442#[async_trait]
443impl InstrumentSource for MarketDataConnector {
444 async fn find(&self, q: InstrumentQuery) -> Result<Vec<Instrument>, Error> {
445 self.acquire("GetSecurities")?;
446 match q {
447 InstrumentQuery::ByTicker { ticker, exchange } => {
448 let mut query = vec![("query", ticker.clone()), ("limit", "100".to_string())];
449 if let Some(e) = exchange {
450 query.push(("exchange", e));
451 }
452 query.push(("includeNonBaseBoards", "true".to_string()));
454 let list = self
455 .inner
456 .http
457 .get_securities(&query)
458 .await
459 .map_err(map_err)?;
460 Ok(list
461 .iter()
462 .filter(|s| s.symbol.eq_ignore_ascii_case(&ticker))
463 .map(instrument)
464 .collect())
465 }
466 InstrumentQuery::ByIsin(_) => Err(Error::Unsupported {
467 capability: "find_by_isin".into(),
468 }),
469 InstrumentQuery::ByKind(kind) => self.find_by_kind(kind).await,
470 }
471 }
472
473 async fn get(&self, id: &InstrumentId) -> Result<Instrument, Error> {
474 self.acquire("GetSecurity")?;
475 let (ticker, board) = ticker_board(id)?;
476 let info = self
477 .inner
478 .http
479 .get_security(exchange_of_board(&board), &ticker, Some(&board))
480 .await
481 .map_err(map_err)?;
482 Ok(instrument(&info))
483 }
484
485 async fn trading_params(&self, id: &InstrumentId) -> Result<TradingParams, Error> {
486 self.acquire("GetSecurity")?;
487 let (ticker, board) = ticker_board(id)?;
488 let info = self
489 .inner
490 .http
491 .get_security(exchange_of_board(&board), &ticker, Some(&board))
492 .await
493 .map_err(map_err)?;
494 Ok(trading_params(&info))
495 }
496
497 async fn trading_status(&self, id: &InstrumentId) -> Result<TradingStatus, Error> {
498 self.acquire("GetTradingStatus")?;
499 let (ticker, board) = ticker_board(id)?;
500 let info = self
501 .inner
502 .http
503 .get_security(exchange_of_board(&board), &ticker, Some(&board))
504 .await
505 .map_err(map_err)?;
506 let mut status = trading_status(&info, Some(unix_nanos(chrono::Utc::now())));
507 status.instrument = id.clone();
509 Ok(status)
510 }
511
512 async fn schedule(&self, _exchange: &str, _range: TimeRange) -> Result<Schedule, Error> {
513 Err(Error::Unsupported {
516 capability: "schedule".into(),
517 })
518 }
519}
520
521impl MarketDataConnector {
522 async fn find_by_kind(&self, kind: InstrumentKind) -> Result<Vec<Instrument>, Error> {
524 let sector = match kind {
525 InstrumentKind::Stock
526 | InstrumentKind::Bond
527 | InstrumentKind::Etf
528 | InstrumentKind::MutualFund => "FOND",
529 InstrumentKind::Future | InstrumentKind::Option => "FORTS",
530 InstrumentKind::Currency => "CURR",
531 InstrumentKind::Perpetual => {
532 return Err(Error::Unsupported {
533 capability: "find_perpetuals_by_kind".into(),
534 });
535 }
536 InstrumentKind::Other => {
537 return Err(Error::Unsupported {
538 capability: "find_other_by_kind".into(),
539 });
540 }
541 };
542 let mut all = Vec::new();
543 for page in 0..MAX_KIND_PAGES {
544 if page > 0 {
545 self.acquire("GetSecurities")?;
546 }
547 let query = [
548 ("sector", sector.to_string()),
549 ("limit", KIND_PAGE_SIZE.to_string()),
550 ("offset", (page * KIND_PAGE_SIZE).to_string()),
551 ];
552 let batch = self
553 .inner
554 .http
555 .get_securities(&query)
556 .await
557 .map_err(map_err)?;
558 let full = batch.len() >= KIND_PAGE_SIZE as usize;
559 all.extend(
560 batch
561 .iter()
562 .filter(|s| crate::map::instrument::kind_of(s) == kind)
563 .map(instrument),
564 );
565 if !full {
566 return Ok(all);
567 }
568 }
569 Err(Error::Transport(brokapi_core::TransportError::Message(
570 format!(
571 "the list of instruments of this kind is longer than {MAX_KIND_PAGES} pages: not returned in part"
572 ),
573 )))
574 }
575}