longbridge/quote/context.rs
1use std::{
2 str::FromStr,
3 sync::{Arc, RwLock},
4 time::Duration,
5};
6
7use longbridge_httpcli::{DcRegion, HttpClient, Json, Method};
8use longbridge_proto::quote;
9use longbridge_wscli::WsClientError;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize, de::DeserializeOwned};
12use time::{Date, PrimitiveDateTime};
13use tokio::sync::{mpsc, oneshot};
14use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
15
16use crate::{
17 Config, Error, Language, Market, QuoteTransport, Result,
18 quote::{
19 AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine,
20 FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature,
21 MarketTradingDays, MarketTradingSession, OptionChainContract, OptionDirection,
22 OptionExpiryCycleType, OptionQuote, OptionStandardAttr, OptionVolumeDaily,
23 OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, PushEvent,
24 QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup,
25 RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth,
26 SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsItem,
27 ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, Subscription, Trade,
28 TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup,
29 cache::{Cache, CacheWithKey},
30 cmd_code,
31 core::{Command, Core, UserProfile},
32 http_json, overnight,
33 sub_flags::SubFlags,
34 types::{
35 FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, PinnedMode,
36 SecuritiesUpdateMode, SortOrderType, WarrantSortBy, WarrantStatus,
37 },
38 utils::{format_date, parse_date},
39 },
40 serde_utils,
41};
42
43const RETRY_COUNT: usize = 3;
44const PARTICIPANT_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
45
46fn history_offset_request(
47 symbol: String,
48 period: Period,
49 adjust_type: AdjustType,
50 forward: bool,
51 time: Option<PrimitiveDateTime>,
52 count: usize,
53 trade_sessions: TradeSessions,
54) -> quote::SecurityHistoryCandlestickRequest {
55 quote::SecurityHistoryCandlestickRequest {
56 symbol,
57 period: period.into(),
58 adjust_type: adjust_type.into(),
59 query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
60 offset_request: Some(quote::security_history_candlestick_request::OffsetQuery {
61 direction: if forward {
62 quote::Direction::Forward
63 } else {
64 quote::Direction::Backward
65 }
66 .into(),
67 date: time
68 .map(|time| {
69 format!(
70 "{:04}{:02}{:02}",
71 time.year(),
72 time.month() as u8,
73 time.day()
74 )
75 })
76 .unwrap_or_default(),
77 minute: time
78 .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
79 .unwrap_or_default(),
80 count: count as i32,
81 }),
82 date_request: None,
83 trade_session: trade_sessions as i32,
84 }
85}
86
87/// Convert a Unix-seconds string (or integer string) to an RFC 3339 timestamp.
88/// If parsing fails, the original string is returned unchanged.
89fn unix_secs_to_rfc3339(s: &str) -> String {
90 s.parse::<i64>()
91 .ok()
92 .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok())
93 .map(|dt| {
94 use time::format_description::well_known::Rfc3339;
95 dt.format(&Rfc3339).unwrap_or_default()
96 })
97 .unwrap_or_else(|| s.to_string())
98}
99const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
100const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
101const TRADING_SESSION_CACHE_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2);
102
103struct InnerQuoteContext {
104 language: Language,
105 quote_transport: QuoteTransport,
106 enable_overnight: bool,
107 http_cli: HttpClient,
108 command_tx: mpsc::UnboundedSender<Command>,
109 /// Kept alive only so the background `Core::run` task can observe the
110 /// context being dropped (via this channel closing) and stop reconnecting.
111 _shutdown_tx: mpsc::UnboundedSender<()>,
112 cache_participants: Cache<Vec<ParticipantInfo>>,
113 cache_issuers: Cache<Vec<IssuerInfo>>,
114 cache_option_chain_expiry_date_list: CacheWithKey<String, Vec<Date>>,
115 cache_trading_session: Cache<Vec<MarketTradingSession>>,
116 user_profile: Arc<RwLock<Option<UserProfile>>>,
117 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
118}
119
120impl Drop for InnerQuoteContext {
121 fn drop(&mut self) {
122 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
123 tracing::info!("quote context dropped");
124 });
125 }
126}
127
128/// Quote context
129#[derive(Clone)]
130pub struct QuoteContext(Arc<InnerQuoteContext>);
131
132impl QuoteContext {
133 /// Create a `QuoteContext`
134 pub fn new(config: Arc<Config>) -> (Self, mpsc::UnboundedReceiver<PushEvent>) {
135 let log_subscriber = config.create_log_subscriber("quote");
136
137 dispatcher::with_default(&log_subscriber.clone().into(), || {
138 tracing::info!(
139 language = ?config.language,
140 enable_overnight = ?config.enable_overnight,
141 push_candlestick_mode = ?config.push_candlestick_mode,
142 enable_print_quote_packages = ?config.enable_print_quote_packages,
143 quote_transport = ?config.quote_transport,
144 "creating quote context"
145 );
146 });
147
148 let language = config.language;
149 let quote_transport = config.quote_transport;
150 let enable_overnight = config.enable_overnight.unwrap_or_default();
151 let http_cli = config.create_http_client();
152 let (command_tx, command_rx) = mpsc::unbounded_channel();
153 let (push_tx, push_rx) = mpsc::unbounded_channel();
154 let (shutdown_tx, shutdown_rx) = mpsc::unbounded_channel();
155 let user_profile = Arc::new(RwLock::new(None::<UserProfile>));
156 let core = Core::new(config, command_rx, push_tx, user_profile.clone());
157 crate::runtime::RUNTIME.handle().spawn(
158 core.run(shutdown_rx)
159 .with_subscriber(log_subscriber.clone()),
160 );
161
162 dispatcher::with_default(&log_subscriber.clone().into(), || {
163 tracing::info!("quote context created");
164 });
165
166 (
167 QuoteContext(Arc::new(InnerQuoteContext {
168 language,
169 quote_transport,
170 enable_overnight,
171 http_cli,
172 command_tx,
173 _shutdown_tx: shutdown_tx,
174 cache_participants: Cache::new(PARTICIPANT_INFO_CACHE_TIMEOUT),
175 cache_issuers: Cache::new(ISSUER_INFO_CACHE_TIMEOUT),
176 cache_option_chain_expiry_date_list: CacheWithKey::new(
177 OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT,
178 ),
179 cache_trading_session: Cache::new(TRADING_SESSION_CACHE_TIMEOUT),
180 user_profile,
181 log_subscriber,
182 })),
183 push_rx,
184 )
185 }
186
187 /// Returns the log subscriber
188 #[inline]
189 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
190 self.0.log_subscriber.clone()
191 }
192
193 async fn ensure_user_profile(&self) -> Result<()> {
194 if self.0.user_profile.read().unwrap().is_some() {
195 return Ok(());
196 }
197 let (reply_tx, reply_rx) = oneshot::channel();
198 self.0
199 .command_tx
200 .send(Command::EnsureConnected { reply_tx })
201 .map_err(|_| WsClientError::ClientClosed)?;
202 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
203 }
204
205 /// Returns the member ID
206 pub async fn member_id(&self) -> Result<i64> {
207 self.ensure_user_profile().await?;
208 Ok(self
209 .0
210 .user_profile
211 .read()
212 .unwrap()
213 .as_ref()
214 .unwrap()
215 .member_id)
216 }
217
218 /// Returns the quote level
219 pub async fn quote_level(&self) -> Result<String> {
220 self.ensure_user_profile().await?;
221 Ok(self
222 .0
223 .user_profile
224 .read()
225 .unwrap()
226 .as_ref()
227 .unwrap()
228 .quote_level
229 .clone())
230 }
231
232 /// Returns the quote package details
233 pub async fn quote_package_details(&self) -> Result<Vec<QuotePackageDetail>> {
234 self.ensure_user_profile().await?;
235 Ok(self
236 .0
237 .user_profile
238 .read()
239 .unwrap()
240 .as_ref()
241 .unwrap()
242 .quote_package_details
243 .clone())
244 }
245
246 /// Send a raw request
247 async fn request_raw(&self, command_code: u8, body: Vec<u8>) -> Result<Vec<u8>> {
248 for _ in 0..RETRY_COUNT {
249 let (reply_tx, reply_rx) = oneshot::channel();
250 self.0
251 .command_tx
252 .send(Command::Request {
253 command_code,
254 body: body.clone(),
255 reply_tx,
256 })
257 .map_err(|_| WsClientError::ClientClosed)?;
258 let res = reply_rx.await.map_err(|_| WsClientError::ClientClosed)?;
259
260 match res {
261 Ok(resp) => return Ok(resp),
262 Err(Error::WsClient(WsClientError::Cancelled)) => {}
263 Err(err) => return Err(err),
264 }
265 }
266
267 Err(Error::WsClient(WsClientError::RequestTimeout))
268 }
269
270 /// Send a request `T` to get a response `R`
271 ///
272 /// With [`QuoteTransport::Http`], a command that has a REST equivalent is
273 /// sent as `POST /quote/*` with the same message as a JSON body, and the
274 /// JSON response is decoded into the same message type, so callers are
275 /// transport-agnostic.
276 async fn request<T, R>(&self, command_code: u8, req: T) -> Result<R>
277 where
278 T: prost::Message + Serialize,
279 R: prost::Message + Default + DeserializeOwned,
280 {
281 if let Some(path) = self.http_path(command_code) {
282 return self.http_request(path, req).await;
283 }
284
285 let resp = self.request_raw(command_code, req.encode_to_vec()).await?;
286 Ok(R::decode(&*resp)?)
287 }
288
289 /// Send a request to get a response `R`
290 async fn request_without_body<R>(&self, command_code: u8) -> Result<R>
291 where
292 R: prost::Message + Default + DeserializeOwned,
293 {
294 if let Some(path) = self.http_path(command_code) {
295 return self
296 .http_request(path, serde_json::Value::Object(Default::default()))
297 .await;
298 }
299
300 let resp = self.request_raw(command_code, vec![]).await?;
301 Ok(R::decode(&*resp)?)
302 }
303
304 /// Whether this context must drop US overnight data to match what the
305 /// WebSocket returns: see [`overnight`].
306 fn aligns_overnight(&self) -> bool {
307 self.0.quote_transport == QuoteTransport::Http && !self.0.enable_overnight
308 }
309
310 /// Convert a candlestick page, dropping overnight candlesticks when
311 /// [`Self::aligns_overnight`], `symbol` is a US equity and the query asked
312 /// for all sessions (the only case that both drops and tops up). Returns
313 /// the candlesticks and how many were dropped.
314 fn convert_candlesticks(
315 &self,
316 symbol: &str,
317 trade_sessions: TradeSessions,
318 resp: quote::SecurityCandlestickResponse,
319 ) -> Result<(Vec<Candlestick>, usize)> {
320 if self.aligns_overnight()
321 && trade_sessions == TradeSessions::All
322 && overnight::is_us_equity_symbol(symbol)
323 {
324 overnight::drop_overnight(resp.candlesticks)
325 } else {
326 Ok((
327 resp.candlesticks
328 .into_iter()
329 .map(TryInto::try_into)
330 .collect::<Result<_>>()?,
331 0,
332 ))
333 }
334 }
335
336 /// Convert a raw candlestick page and, on the HTTP transport, top up a
337 /// capped US window after overnight bars were dropped so it covers the
338 /// same bars as the WebSocket (see [`overnight`]). The top-up pages with
339 /// `POST /quote/history-candlesticks` offset queries; a failure there
340 /// fails the call.
341 #[allow(clippy::too_many_arguments)]
342 async fn align_candlesticks(
343 &self,
344 resp: quote::SecurityCandlestickResponse,
345 symbol: &str,
346 period: Period,
347 adjust_type: AdjustType,
348 trade_sessions: TradeSessions,
349 forward: bool,
350 window: overnight::CandlestickWindow,
351 ) -> Result<Vec<Candlestick>> {
352 let target = overnight::window_target(window, &resp.candlesticks);
353 let cursor = overnight::raw_edge(&resp.candlesticks, forward);
354 let (candlesticks, dropped) = self.convert_candlesticks(symbol, trade_sessions, resp)?;
355 let (Some(target), true) = (target, dropped > 0 && trade_sessions == TradeSessions::All)
356 else {
357 return Ok(candlesticks);
358 };
359 let bound = match window {
360 overnight::CandlestickWindow::DateRange { start } => start,
361 overnight::CandlestickWindow::Count(_) => None,
362 };
363 overnight::refill(
364 forward,
365 candlesticks,
366 cursor,
367 target,
368 bound,
369 |anchor, count| {
370 let request = history_offset_request(
371 symbol.to_string(),
372 period,
373 adjust_type,
374 forward,
375 Some(anchor),
376 count,
377 trade_sessions,
378 );
379 async move {
380 let resp: quote::SecurityCandlestickResponse = self
381 .request(cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS, request)
382 .await?;
383 Ok(resp.candlesticks)
384 }
385 },
386 )
387 .await
388 .map(|(candlesticks, round_limit_hit)| {
389 if round_limit_hit {
390 dispatcher::with_default(&self.0.log_subscriber.clone().into(), || {
391 tracing::warn!(
392 symbol,
393 target,
394 got = candlesticks.len(),
395 "overnight top-up hit its round limit; fewer candlesticks than the WebSocket would return"
396 );
397 });
398 }
399 candlesticks
400 })
401 }
402
403 /// Returns the REST path to use for `command_code`, if the HTTP transport
404 /// is selected and the command has a REST equivalent.
405 fn http_path(&self, command_code: u8) -> Option<&'static str> {
406 match self.0.quote_transport {
407 QuoteTransport::Http => cmd_code::http_path(command_code),
408 QuoteTransport::WebSocket => None,
409 }
410 }
411
412 /// `POST` `body` to `path` and decode the proto-JSON response into `R`.
413 ///
414 /// Absent sub-messages (`Option::None`) are omitted from the body instead
415 /// of being sent as `null`, matching how a proto-JSON client would encode
416 /// them.
417 async fn http_request<B, R>(&self, path: &'static str, body: B) -> Result<R>
418 where
419 B: Serialize,
420 R: DeserializeOwned,
421 {
422 let mut body = serde_json::to_value(body)?;
423 http_json::strip_nulls(&mut body);
424 let Json(value) = self
425 .0
426 .http_cli
427 .request(Method::POST, path)
428 .body(Json(body))
429 .response::<Json<serde_json::Value>>()
430 .send()
431 .with_subscriber(self.0.log_subscriber.clone())
432 .await?;
433 Ok(http_json::from_value(value)?)
434 }
435
436 /// Subscribe
437 ///
438 /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/subscribe>
439 ///
440 /// # Examples
441 ///
442 /// ```no_run
443 /// use std::sync::Arc;
444 ///
445 /// use longbridge::{
446 /// Config,
447 /// oauth::OAuthBuilder,
448 /// quote::{QuoteContext, SubFlags},
449 /// };
450 ///
451 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
452 /// let oauth = OAuthBuilder::new("your-client-id")
453 /// .build(|url| println!("Visit: {url}"))
454 /// .await?;
455 /// let config = Arc::new(Config::from_oauth(oauth));
456 /// let (ctx, mut receiver) = QuoteContext::new(config);
457 ///
458 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
459 /// .await?;
460 /// while let Some(msg) = receiver.recv().await {
461 /// println!("{:?}", msg);
462 /// }
463 /// # Ok::<_, Box<dyn std::error::Error>>(())
464 /// # });
465 /// ```
466 pub async fn subscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
467 where
468 I: IntoIterator<Item = T>,
469 T: AsRef<str>,
470 {
471 let (reply_tx, reply_rx) = oneshot::channel();
472 self.0
473 .command_tx
474 .send(Command::Subscribe {
475 symbols: symbols
476 .into_iter()
477 .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
478 .collect(),
479 sub_types: sub_types.into(),
480 reply_tx,
481 })
482 .map_err(|_| WsClientError::ClientClosed)?;
483 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
484 }
485
486 /// Unsubscribe
487 ///
488 /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/unsubscribe>
489 ///
490 /// # Examples
491 ///
492 /// ```no_run
493 /// use std::sync::Arc;
494 ///
495 /// use longbridge::{
496 /// Config,
497 /// oauth::OAuthBuilder,
498 /// quote::{QuoteContext, SubFlags},
499 /// };
500 ///
501 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
502 /// let oauth = OAuthBuilder::new("your-client-id")
503 /// .build(|url| println!("Visit: {url}"))
504 /// .await?;
505 /// let config = Arc::new(Config::from_oauth(oauth));
506 /// let (ctx, _) = QuoteContext::new(config);
507 ///
508 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
509 /// .await?;
510 /// ctx.unsubscribe(["AAPL.US"], SubFlags::QUOTE).await?;
511 /// # Ok::<_, Box<dyn std::error::Error>>(())
512 /// # });
513 /// ```
514 pub async fn unsubscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
515 where
516 I: IntoIterator<Item = T>,
517 T: AsRef<str>,
518 {
519 let (reply_tx, reply_rx) = oneshot::channel();
520 self.0
521 .command_tx
522 .send(Command::Unsubscribe {
523 symbols: symbols
524 .into_iter()
525 .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
526 .collect(),
527 sub_types: sub_types.into(),
528 reply_tx,
529 })
530 .map_err(|_| WsClientError::ClientClosed)?;
531 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
532 }
533
534 /// Subscribe security candlesticks
535 ///
536 /// # Examples
537 ///
538 /// ```no_run
539 /// use std::sync::Arc;
540 ///
541 /// use longbridge::{
542 /// Config,
543 /// oauth::OAuthBuilder,
544 /// quote::{Period, QuoteContext, TradeSessions},
545 /// };
546 ///
547 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
548 /// let oauth = OAuthBuilder::new("your-client-id")
549 /// .build(|url| println!("Visit: {url}"))
550 /// .await?;
551 /// let config = Arc::new(Config::from_oauth(oauth));
552 /// let (ctx, mut receiver) = QuoteContext::new(config);
553 ///
554 /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
555 /// .await?;
556 /// while let Some(msg) = receiver.recv().await {
557 /// println!("{:?}", msg);
558 /// }
559 /// # Ok::<_, Box<dyn std::error::Error>>(())
560 /// # });
561 /// ```
562 pub async fn subscribe_candlesticks<T>(
563 &self,
564 symbol: T,
565 period: Period,
566 trade_sessions: TradeSessions,
567 ) -> Result<Vec<Candlestick>>
568 where
569 T: AsRef<str>,
570 {
571 let (reply_tx, reply_rx) = oneshot::channel();
572 self.0
573 .command_tx
574 .send(Command::SubscribeCandlesticks {
575 symbol: normalize_symbol(symbol.as_ref()).into(),
576 period,
577 trade_sessions,
578 reply_tx,
579 })
580 .map_err(|_| WsClientError::ClientClosed)?;
581 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
582 }
583
584 /// Unsubscribe security candlesticks
585 pub async fn unsubscribe_candlesticks<T>(&self, symbol: T, period: Period) -> Result<()>
586 where
587 T: AsRef<str>,
588 {
589 let (reply_tx, reply_rx) = oneshot::channel();
590 self.0
591 .command_tx
592 .send(Command::UnsubscribeCandlesticks {
593 symbol: normalize_symbol(symbol.as_ref()).into(),
594 period,
595 reply_tx,
596 })
597 .map_err(|_| WsClientError::ClientClosed)?;
598 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
599 }
600
601 /// Get subscription information
602 ///
603 /// # Examples
604 ///
605 /// ```no_run
606 /// use std::sync::Arc;
607 ///
608 /// use longbridge::{
609 /// Config,
610 /// oauth::OAuthBuilder,
611 /// quote::{QuoteContext, SubFlags},
612 /// };
613 ///
614 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
615 /// let oauth = OAuthBuilder::new("your-client-id")
616 /// .build(|url| println!("Visit: {url}"))
617 /// .await?;
618 /// let config = Arc::new(Config::from_oauth(oauth));
619 /// let (ctx, _) = QuoteContext::new(config);
620 ///
621 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
622 /// .await?;
623 /// let resp = ctx.subscriptions().await?;
624 /// println!("{:?}", resp);
625 /// # Ok::<_, Box<dyn std::error::Error>>(())
626 /// # });
627 /// ```
628 pub async fn subscriptions(&self) -> Result<Vec<Subscription>> {
629 let (reply_tx, reply_rx) = oneshot::channel();
630 self.0
631 .command_tx
632 .send(Command::Subscriptions { reply_tx })
633 .map_err(|_| WsClientError::ClientClosed)?;
634 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
635 }
636
637 /// Get basic information of securities
638 ///
639 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/static>
640 ///
641 /// # Examples
642 ///
643 /// ```no_run
644 /// use std::sync::Arc;
645 ///
646 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
647 ///
648 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
649 /// let oauth = OAuthBuilder::new("your-client-id")
650 /// .build(|url| println!("Visit: {url}"))
651 /// .await?;
652 /// let config = Arc::new(Config::from_oauth(oauth));
653 /// let (ctx, _) = QuoteContext::new(config);
654 ///
655 /// let resp = ctx
656 /// .static_info(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
657 /// .await?;
658 /// println!("{:?}", resp);
659 /// # Ok::<_, Box<dyn std::error::Error>>(())
660 /// # });
661 /// ```
662 pub async fn static_info<I, T>(&self, symbols: I) -> Result<Vec<SecurityStaticInfo>>
663 where
664 I: IntoIterator<Item = T>,
665 T: Into<String>,
666 {
667 let resp: quote::SecurityStaticInfoResponse = self
668 .request(
669 cmd_code::GET_BASIC_INFO,
670 quote::MultiSecurityRequest {
671 symbol: symbols.into_iter().map(Into::into).collect(),
672 },
673 )
674 .await?;
675 resp.secu_static_info
676 .into_iter()
677 .map(TryInto::try_into)
678 .collect()
679 }
680
681 /// Get quote of securities
682 ///
683 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/quote>
684 ///
685 /// # Examples
686 ///
687 /// ```no_run
688 /// use std::sync::Arc;
689 ///
690 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
691 ///
692 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
693 /// let oauth = OAuthBuilder::new("your-client-id")
694 /// .build(|url| println!("Visit: {url}"))
695 /// .await?;
696 /// let config = Arc::new(Config::from_oauth(oauth));
697 /// let (ctx, _) = QuoteContext::new(config);
698 ///
699 /// let resp = ctx
700 /// .quote(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
701 /// .await?;
702 /// println!("{:?}", resp);
703 /// # Ok::<_, Box<dyn std::error::Error>>(())
704 /// # });
705 /// ```
706 pub async fn quote<I, T>(&self, symbols: I) -> Result<Vec<SecurityQuote>>
707 where
708 I: IntoIterator<Item = T>,
709 T: Into<String>,
710 {
711 let resp: quote::SecurityQuoteResponse = self
712 .request(
713 cmd_code::GET_REALTIME_QUOTE,
714 quote::MultiSecurityRequest {
715 symbol: symbols.into_iter().map(Into::into).collect(),
716 },
717 )
718 .await?;
719 resp.secu_quote
720 .into_iter()
721 .map(|mut quote| {
722 // See `overnight`: HTTP always returns overnight quotes.
723 if self.aligns_overnight() && overnight::is_us_equity_symbol("e.symbol) {
724 quote.over_night_quote = None;
725 }
726 quote.try_into()
727 })
728 .collect()
729 }
730
731 /// Get quote of option securities
732 ///
733 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/option-quote>
734 ///
735 /// # Examples
736 ///
737 /// ```no_run
738 /// use std::sync::Arc;
739 ///
740 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
741 ///
742 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
743 /// let oauth = OAuthBuilder::new("your-client-id")
744 /// .build(|url| println!("Visit: {url}"))
745 /// .await?;
746 /// let config = Arc::new(Config::from_oauth(oauth));
747 /// let (ctx, _) = QuoteContext::new(config);
748 ///
749 /// let resp = ctx.option_quote(["AAPL230317P160000.US"]).await?;
750 /// println!("{:?}", resp);
751 /// # Ok::<_, Box<dyn std::error::Error>>(())
752 /// # });
753 /// ```
754 pub async fn option_quote<I, T>(&self, symbols: I) -> Result<Vec<OptionQuote>>
755 where
756 I: IntoIterator<Item = T>,
757 T: Into<String>,
758 {
759 let resp: quote::OptionQuoteResponse = self
760 .request(
761 cmd_code::GET_REALTIME_OPTION_QUOTE,
762 quote::MultiSecurityRequest {
763 symbol: symbols.into_iter().map(Into::into).collect(),
764 },
765 )
766 .await?;
767 resp.secu_quote.into_iter().map(TryInto::try_into).collect()
768 }
769
770 /// Get quote of warrant securities
771 ///
772 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/warrant-quote>
773 ///
774 /// # Examples
775 ///
776 /// ```no_run
777 /// use std::sync::Arc;
778 ///
779 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
780 ///
781 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
782 /// let oauth = OAuthBuilder::new("your-client-id")
783 /// .build(|url| println!("Visit: {url}"))
784 /// .await?;
785 /// let config = Arc::new(Config::from_oauth(oauth));
786 /// let (ctx, _) = QuoteContext::new(config);
787 ///
788 /// let resp = ctx.warrant_quote(["21125.HK"]).await?;
789 /// println!("{:?}", resp);
790 /// # Ok::<_, Box<dyn std::error::Error>>(())
791 /// # });
792 /// ```
793 pub async fn warrant_quote<I, T>(&self, symbols: I) -> Result<Vec<WarrantQuote>>
794 where
795 I: IntoIterator<Item = T>,
796 T: Into<String>,
797 {
798 let resp: quote::WarrantQuoteResponse = self
799 .request(
800 cmd_code::GET_REALTIME_WARRANT_QUOTE,
801 quote::MultiSecurityRequest {
802 symbol: symbols.into_iter().map(Into::into).collect(),
803 },
804 )
805 .await?;
806 resp.secu_quote.into_iter().map(TryInto::try_into).collect()
807 }
808
809 /// Get security depth
810 ///
811 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/depth>
812 ///
813 /// # Examples
814 ///
815 /// ```no_run
816 /// use std::sync::Arc;
817 ///
818 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
819 ///
820 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
821 /// let oauth = OAuthBuilder::new("your-client-id")
822 /// .build(|url| println!("Visit: {url}"))
823 /// .await?;
824 /// let config = Arc::new(Config::from_oauth(oauth));
825 /// let (ctx, _) = QuoteContext::new(config);
826 ///
827 /// let resp = ctx.depth("700.HK").await?;
828 /// println!("{:?}", resp);
829 /// # Ok::<_, Box<dyn std::error::Error>>(())
830 /// # });
831 /// ```
832 pub async fn depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
833 let resp: quote::SecurityDepthResponse = self
834 .request(
835 cmd_code::GET_SECURITY_DEPTH,
836 quote::SecurityRequest {
837 symbol: symbol.into(),
838 },
839 )
840 .await?;
841 Ok(SecurityDepth {
842 asks: resp
843 .ask
844 .into_iter()
845 .map(TryInto::try_into)
846 .collect::<Result<Vec<_>>>()?,
847 bids: resp
848 .bid
849 .into_iter()
850 .map(TryInto::try_into)
851 .collect::<Result<Vec<_>>>()?,
852 })
853 }
854
855 /// Get security brokers
856 ///
857 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/brokers>
858 ///
859 /// # Examples
860 ///
861 /// ```no_run
862 /// use std::sync::Arc;
863 ///
864 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
865 ///
866 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
867 /// let oauth = OAuthBuilder::new("your-client-id")
868 /// .build(|url| println!("Visit: {url}"))
869 /// .await?;
870 /// let config = Arc::new(Config::from_oauth(oauth));
871 /// let (ctx, _) = QuoteContext::new(config);
872 ///
873 /// let resp = ctx.brokers("700.HK").await?;
874 /// println!("{:?}", resp);
875 /// # Ok::<_, Box<dyn std::error::Error>>(())
876 /// # });
877 /// ```
878 pub async fn brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
879 // Broker queue is served only by the AP data center; short-circuit a
880 // non-AP session with the same unified error the HTTP path returns.
881 let current = self.0.http_cli.dc_region().await;
882 if !current.allows(longbridge_httpcli::DcRegion::Ap) {
883 return Err(longbridge_httpcli::HttpClientError::DcRegionRestricted {
884 path: "quote/brokers".to_string(),
885 required: longbridge_httpcli::DcRegion::Ap,
886 current,
887 }
888 .into());
889 }
890 let resp: quote::SecurityBrokersResponse = self
891 .request(
892 cmd_code::GET_SECURITY_BROKERS,
893 quote::SecurityRequest {
894 symbol: symbol.into(),
895 },
896 )
897 .await?;
898 Ok(SecurityBrokers {
899 ask_brokers: resp.ask_brokers.into_iter().map(Into::into).collect(),
900 bid_brokers: resp.bid_brokers.into_iter().map(Into::into).collect(),
901 })
902 }
903
904 /// Get participants
905 ///
906 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/broker-ids>
907 ///
908 /// # Examples
909 ///
910 /// ```no_run
911 /// use std::sync::Arc;
912 ///
913 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
914 ///
915 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
916 /// let oauth = OAuthBuilder::new("your-client-id")
917 /// .build(|url| println!("Visit: {url}"))
918 /// .await?;
919 /// let config = Arc::new(Config::from_oauth(oauth));
920 /// let (ctx, _) = QuoteContext::new(config);
921 ///
922 /// let resp = ctx.participants().await?;
923 /// println!("{:?}", resp);
924 /// # Ok::<_, Box<dyn std::error::Error>>(())
925 /// # });
926 /// ```
927 pub async fn participants(&self) -> Result<Vec<ParticipantInfo>> {
928 self.0
929 .cache_participants
930 .get_or_update(|| async {
931 let resp = self
932 .request_without_body::<quote::ParticipantBrokerIdsResponse>(
933 cmd_code::GET_BROKER_IDS,
934 )
935 .await?;
936
937 Ok(resp
938 .participant_broker_numbers
939 .into_iter()
940 .map(Into::into)
941 .collect())
942 })
943 .await
944 }
945
946 /// Get security trades
947 ///
948 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade>
949 ///
950 /// # Examples
951 ///
952 /// ```no_run
953 /// use std::sync::Arc;
954 ///
955 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
956 ///
957 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
958 /// let oauth = OAuthBuilder::new("your-client-id")
959 /// .build(|url| println!("Visit: {url}"))
960 /// .await?;
961 /// let config = Arc::new(Config::from_oauth(oauth));
962 /// let (ctx, _) = QuoteContext::new(config);
963 ///
964 /// let resp = ctx.trades("700.HK", 10).await?;
965 /// println!("{:?}", resp);
966 /// # Ok::<_, Box<dyn std::error::Error>>(())
967 /// # });
968 /// ```
969 pub async fn trades(&self, symbol: impl Into<String>, count: usize) -> Result<Vec<Trade>> {
970 let resp: quote::SecurityTradeResponse = self
971 .request(
972 cmd_code::GET_SECURITY_TRADES,
973 quote::SecurityTradeRequest {
974 symbol: symbol.into(),
975 count: count as i32,
976 },
977 )
978 .await?;
979 let trades = resp
980 .trades
981 .into_iter()
982 .map(TryInto::try_into)
983 .collect::<Result<Vec<_>>>()?;
984 Ok(trades)
985 }
986
987 /// Get security intraday lines
988 ///
989 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/intraday>
990 ///
991 /// # Examples
992 ///
993 /// ```no_run
994 /// use std::sync::Arc;
995 ///
996 /// use longbridge::{
997 /// Config,
998 /// oauth::OAuthBuilder,
999 /// quote::{QuoteContext, TradeSessions},
1000 /// };
1001 ///
1002 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1003 /// let oauth = OAuthBuilder::new("your-client-id")
1004 /// .build(|url| println!("Visit: {url}"))
1005 /// .await?;
1006 /// let config = Arc::new(Config::from_oauth(oauth));
1007 /// let (ctx, _) = QuoteContext::new(config);
1008 ///
1009 /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
1010 /// println!("{:?}", resp);
1011 /// # Ok::<_, Box<dyn std::error::Error>>(())
1012 /// # });
1013 /// ```
1014 pub async fn intraday(
1015 &self,
1016 symbol: impl Into<String>,
1017 trade_sessions: TradeSessions,
1018 ) -> Result<Vec<IntradayLine>> {
1019 let symbol = symbol.into();
1020 let drop_overnight = self.aligns_overnight() && overnight::is_us_equity_symbol(&symbol);
1021 let resp: quote::SecurityIntradayResponse = self
1022 .request(
1023 cmd_code::GET_SECURITY_INTRADAY,
1024 quote::SecurityIntradayRequest {
1025 symbol,
1026 trade_session: trade_sessions as i32,
1027 },
1028 )
1029 .await?;
1030 let lines = resp
1031 .lines
1032 .into_iter()
1033 .map(TryInto::<IntradayLine>::try_into)
1034 .filter(|line| {
1035 // See `overnight`: HTTP always returns the overnight session.
1036 !drop_overnight
1037 || !matches!(line, Ok(line) if overnight::is_us_overnight(line.timestamp))
1038 })
1039 .collect::<Result<Vec<_>>>()?;
1040 Ok(lines)
1041 }
1042
1043 /// Get security candlesticks
1044 ///
1045 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/candlestick>
1046 ///
1047 /// # Examples
1048 ///
1049 /// ```no_run
1050 /// use std::sync::Arc;
1051 ///
1052 /// use longbridge::{
1053 /// Config,
1054 /// oauth::OAuthBuilder,
1055 /// quote::{AdjustType, Period, QuoteContext, TradeSessions},
1056 /// };
1057 ///
1058 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1059 /// let oauth = OAuthBuilder::new("your-client-id")
1060 /// .build(|url| println!("Visit: {url}"))
1061 /// .await?;
1062 /// let config = Arc::new(Config::from_oauth(oauth));
1063 /// let (ctx, _) = QuoteContext::new(config);
1064 ///
1065 /// let resp = ctx
1066 /// .candlesticks(
1067 /// "700.HK",
1068 /// Period::Day,
1069 /// 10,
1070 /// AdjustType::NoAdjust,
1071 /// TradeSessions::Intraday,
1072 /// )
1073 /// .await?;
1074 /// println!("{:?}", resp);
1075 /// # Ok::<_, Box<dyn std::error::Error>>(())
1076 /// # });
1077 /// ```
1078 pub async fn candlesticks(
1079 &self,
1080 symbol: impl Into<String>,
1081 period: Period,
1082 count: usize,
1083 adjust_type: AdjustType,
1084 trade_sessions: TradeSessions,
1085 ) -> Result<Vec<Candlestick>> {
1086 let symbol = symbol.into();
1087 let resp: quote::SecurityCandlestickResponse = self
1088 .request(
1089 cmd_code::GET_SECURITY_CANDLESTICKS,
1090 quote::SecurityCandlestickRequest {
1091 symbol: symbol.clone(),
1092 period: period.into(),
1093 count: count as i32,
1094 adjust_type: adjust_type.into(),
1095 trade_session: trade_sessions as i32,
1096 },
1097 )
1098 .await?;
1099 self.align_candlesticks(
1100 resp,
1101 &symbol,
1102 period,
1103 adjust_type,
1104 trade_sessions,
1105 false,
1106 overnight::CandlestickWindow::Count(count),
1107 )
1108 .await
1109 }
1110
1111 /// Get security history candlesticks by offset
1112 #[allow(clippy::too_many_arguments)]
1113 pub async fn history_candlesticks_by_offset(
1114 &self,
1115 symbol: impl Into<String>,
1116 period: Period,
1117 adjust_type: AdjustType,
1118 forward: bool,
1119 time: Option<PrimitiveDateTime>,
1120 count: usize,
1121 trade_sessions: TradeSessions,
1122 ) -> Result<Vec<Candlestick>> {
1123 let symbol = symbol.into();
1124 let resp: quote::SecurityCandlestickResponse = self
1125 .request(
1126 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
1127 history_offset_request(
1128 symbol.clone(),
1129 period,
1130 adjust_type,
1131 forward,
1132 time,
1133 count,
1134 trade_sessions,
1135 ),
1136 )
1137 .await?;
1138 self.align_candlesticks(
1139 resp,
1140 &symbol,
1141 period,
1142 adjust_type,
1143 trade_sessions,
1144 forward,
1145 overnight::CandlestickWindow::Count(count),
1146 )
1147 .await
1148 }
1149
1150 /// Get security history candlesticks by date
1151 pub async fn history_candlesticks_by_date(
1152 &self,
1153 symbol: impl Into<String>,
1154 period: Period,
1155 adjust_type: AdjustType,
1156 start: Option<Date>,
1157 end: Option<Date>,
1158 trade_sessions: TradeSessions,
1159 ) -> Result<Vec<Candlestick>> {
1160 let symbol = symbol.into();
1161 let resp: quote::SecurityCandlestickResponse = self
1162 .request(
1163 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
1164 quote::SecurityHistoryCandlestickRequest {
1165 symbol: symbol.clone(),
1166 period: period.into(),
1167 adjust_type: adjust_type.into(),
1168 query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
1169 offset_request: None,
1170 date_request: Some(quote::security_history_candlestick_request::DateQuery {
1171 start_date: start
1172 .map(|date| {
1173 format!(
1174 "{:04}{:02}{:02}",
1175 date.year(),
1176 date.month() as u8,
1177 date.day()
1178 )
1179 })
1180 .unwrap_or_default(),
1181 end_date: end
1182 .map(|date| {
1183 format!(
1184 "{:04}{:02}{:02}",
1185 date.year(),
1186 date.month() as u8,
1187 date.day()
1188 )
1189 })
1190 .unwrap_or_default(),
1191 }),
1192 trade_session: trade_sessions as i32,
1193 },
1194 )
1195 .await?;
1196 self.align_candlesticks(
1197 resp,
1198 &symbol,
1199 period,
1200 adjust_type,
1201 trade_sessions,
1202 false,
1203 overnight::CandlestickWindow::DateRange { start },
1204 )
1205 .await
1206 }
1207
1208 /// Get option chain expiry date list
1209 ///
1210 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date>
1211 ///
1212 /// # Examples
1213 ///
1214 /// ```no_run
1215 /// use std::sync::Arc;
1216 ///
1217 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1218 ///
1219 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1220 /// let oauth = OAuthBuilder::new("your-client-id")
1221 /// .build(|url| println!("Visit: {url}"))
1222 /// .await?;
1223 /// let config = Arc::new(Config::from_oauth(oauth));
1224 /// let (ctx, _) = QuoteContext::new(config);
1225 ///
1226 /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
1227 /// println!("{:?}", resp);
1228 /// # Ok::<_, Box<dyn std::error::Error>>(())
1229 /// # });
1230 /// ```
1231 pub async fn option_chain_expiry_date_list(
1232 &self,
1233 symbol: impl Into<String>,
1234 ) -> Result<Vec<Date>> {
1235 self.0
1236 .cache_option_chain_expiry_date_list
1237 .get_or_update(symbol.into(), |symbol| async {
1238 let resp: quote::OptionChainDateListResponse = self
1239 .request(
1240 cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
1241 quote::SecurityRequest { symbol },
1242 )
1243 .await?;
1244 resp.expiry_date
1245 .iter()
1246 .map(|value| {
1247 parse_date(value).map_err(|err| Error::parse_field_error("date", err))
1248 })
1249 .collect::<Result<Vec<_>>>()
1250 })
1251 .await
1252 }
1253
1254 /// Get the option contract list of an underlying security for a given
1255 /// expiry date
1256 ///
1257 /// Every contract is an independent entry: calls and puts are not paired,
1258 /// so a strike price that is listed on one side only yields a single entry.
1259 ///
1260 /// `standard_only` filters out the legacy contracts produced by corporate
1261 /// actions. `true` returns standard contracts only; `false` returns
1262 /// everything, including the contracts carrying
1263 /// [`OptionStandardAttr::Old`].
1264 ///
1265 /// Path: `POST /v1/quote/options/strikes`
1266 ///
1267 /// # Examples
1268 ///
1269 /// ```no_run
1270 /// use std::sync::Arc;
1271 ///
1272 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1273 /// use time::macros::date;
1274 ///
1275 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1276 /// let oauth = OAuthBuilder::new("your-client-id")
1277 /// .build(|url| println!("Visit: {url}"))
1278 /// .await?;
1279 /// let config = Arc::new(Config::from_oauth(oauth));
1280 /// let (ctx, _) = QuoteContext::new(config);
1281 ///
1282 /// let resp = ctx
1283 /// .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20), false)
1284 /// .await?;
1285 /// println!("{:?}", resp);
1286 /// # Ok::<_, Box<dyn std::error::Error>>(())
1287 /// # });
1288 /// ```
1289 pub async fn option_chain_info_by_date(
1290 &self,
1291 symbol: impl Into<String>,
1292 expiry_date: Date,
1293 standard_only: bool,
1294 ) -> Result<Vec<OptionChainContract>> {
1295 #[derive(Debug, Serialize)]
1296 struct Request {
1297 symbol: String,
1298 expiry_date: String,
1299 // `false` and an omitted field mean the same thing to the
1300 // endpoint, so send nothing rather than `"standard_only": false`.
1301 #[serde(skip_serializing_if = "std::ops::Not::not")]
1302 standard_only: bool,
1303 }
1304
1305 #[derive(Debug, Deserialize)]
1306 struct RawOptionChainContract {
1307 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1308 symbol: String,
1309 expiry_date: String,
1310 #[serde(with = "crate::serde_utils::decimal_empty_is_0")]
1311 strike_price: Decimal,
1312 direction: String,
1313 // Both of these are documented as an empty string for the common
1314 // case, so tolerate an explicit `null` as well.
1315 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1316 option_type: String,
1317 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1318 standard_attr: String,
1319 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1320 days_to_expiry: i32,
1321 }
1322
1323 #[derive(Debug, Deserialize)]
1324 struct Response {
1325 #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1326 list: Vec<RawOptionChainContract>,
1327 }
1328
1329 self.0
1330 .http_cli
1331 .request(Method::POST, "/v1/quote/options/strikes")
1332 .body(Json(Request {
1333 symbol: symbol.into(),
1334 expiry_date: format_date(expiry_date),
1335 standard_only,
1336 }))
1337 .response::<Json<Response>>()
1338 .send()
1339 .with_subscriber(self.0.log_subscriber.clone())
1340 .await?
1341 .0
1342 .list
1343 .into_iter()
1344 .map(|row| {
1345 Ok(OptionChainContract {
1346 symbol: row.symbol,
1347 expiry_date: parse_date(&row.expiry_date)
1348 .map_err(|err| Error::parse_field_error("expiry_date", err))?,
1349 strike_price: row.strike_price,
1350 direction: OptionDirection::from_str(&row.direction).unwrap_or_default(),
1351 option_type: OptionExpiryCycleType::from_str(&row.option_type)
1352 .unwrap_or_default(),
1353 standard_attr: OptionStandardAttr::from_str(&row.standard_attr)
1354 .unwrap_or_default(),
1355 days_to_expiry: row.days_to_expiry,
1356 })
1357 })
1358 .collect()
1359 }
1360
1361 /// Get warrant issuers
1362 ///
1363 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/issuer>
1364 ///
1365 /// # Examples
1366 ///
1367 /// ```no_run
1368 /// use std::sync::Arc;
1369 ///
1370 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1371 ///
1372 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1373 /// let oauth = OAuthBuilder::new("your-client-id")
1374 /// .build(|url| println!("Visit: {url}"))
1375 /// .await?;
1376 /// let config = Arc::new(Config::from_oauth(oauth));
1377 /// let (ctx, _) = QuoteContext::new(config);
1378 ///
1379 /// let resp = ctx.warrant_issuers().await?;
1380 /// println!("{:?}", resp);
1381 /// # Ok::<_, Box<dyn std::error::Error>>(())
1382 /// # });
1383 /// ```
1384 pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1385 self.0
1386 .cache_issuers
1387 .get_or_update(|| async {
1388 let resp = self
1389 .request_without_body::<quote::IssuerInfoResponse>(
1390 cmd_code::GET_WARRANT_ISSUER_IDS,
1391 )
1392 .await?;
1393 Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1394 })
1395 .await
1396 }
1397
1398 /// Query warrant list
1399 #[allow(clippy::too_many_arguments)]
1400 pub async fn warrant_list(
1401 &self,
1402 symbol: impl Into<String>,
1403 sort_by: WarrantSortBy,
1404 sort_order: SortOrderType,
1405 warrant_type: Option<&[WarrantType]>,
1406 issuer: Option<&[i32]>,
1407 expiry_date: Option<&[FilterWarrantExpiryDate]>,
1408 price_type: Option<&[FilterWarrantInOutBoundsType]>,
1409 status: Option<&[WarrantStatus]>,
1410 ) -> Result<Vec<WarrantInfo>> {
1411 let resp = self
1412 .request::<_, quote::WarrantFilterListResponse>(
1413 cmd_code::GET_FILTERED_WARRANT,
1414 quote::WarrantFilterListRequest {
1415 symbol: symbol.into(),
1416 filter_config: Some(quote::FilterConfig {
1417 sort_by: sort_by.into(),
1418 sort_order: sort_order.into(),
1419 sort_offset: 0,
1420 sort_count: 0,
1421 r#type: warrant_type
1422 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1423 .unwrap_or_default(),
1424 issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1425 expiry_date: expiry_date
1426 .map(|e| e.iter().map(|e| (*e).into()).collect())
1427 .unwrap_or_default(),
1428 price_type: price_type
1429 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1430 .unwrap_or_default(),
1431 status: status
1432 .map(|status| status.iter().map(|status| (*status).into()).collect())
1433 .unwrap_or_default(),
1434 }),
1435 language: self.0.language.into(),
1436 },
1437 )
1438 .await?;
1439 resp.warrant_list
1440 .into_iter()
1441 .map(TryInto::try_into)
1442 .collect::<Result<Vec<_>>>()
1443 }
1444
1445 /// Get trading session of the day
1446 ///
1447 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-session>
1448 ///
1449 /// # Examples
1450 ///
1451 /// ```no_run
1452 /// use std::sync::Arc;
1453 ///
1454 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1455 ///
1456 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1457 /// let oauth = OAuthBuilder::new("your-client-id")
1458 /// .build(|url| println!("Visit: {url}"))
1459 /// .await?;
1460 /// let config = Arc::new(Config::from_oauth(oauth));
1461 /// let (ctx, _) = QuoteContext::new(config);
1462 ///
1463 /// let resp = ctx.trading_session().await?;
1464 /// println!("{:?}", resp);
1465 /// # Ok::<_, Box<dyn std::error::Error>>(())
1466 /// # });
1467 /// ```
1468 pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1469 self.0
1470 .cache_trading_session
1471 .get_or_update(|| async {
1472 let resp = self
1473 .request_without_body::<quote::MarketTradePeriodResponse>(
1474 cmd_code::GET_TRADING_SESSION,
1475 )
1476 .await?;
1477 resp.market_trade_session
1478 .into_iter()
1479 .map(TryInto::try_into)
1480 .collect::<Result<Vec<_>>>()
1481 })
1482 .await
1483 }
1484
1485 /// Get market trading days
1486 ///
1487 /// The interval must be less than one month, and only the most recent year
1488 /// is supported.
1489 ///
1490 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-day>
1491 ///
1492 /// # Examples
1493 ///
1494 /// ```no_run
1495 /// use std::sync::Arc;
1496 ///
1497 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1498 /// use time::macros::date;
1499 ///
1500 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1501 /// let oauth = OAuthBuilder::new("your-client-id")
1502 /// .build(|url| println!("Visit: {url}"))
1503 /// .await?;
1504 /// let config = Arc::new(Config::from_oauth(oauth));
1505 /// let (ctx, _) = QuoteContext::new(config);
1506 ///
1507 /// let resp = ctx
1508 /// .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1509 /// .await?;
1510 /// println!("{:?}", resp);
1511 /// # Ok::<_, Box<dyn std::error::Error>>(())
1512 /// # });
1513 /// ```
1514 pub async fn trading_days(
1515 &self,
1516 market: Market,
1517 begin: Date,
1518 end: Date,
1519 ) -> Result<MarketTradingDays> {
1520 let resp = self
1521 .request::<_, quote::MarketTradeDayResponse>(
1522 cmd_code::GET_TRADING_DAYS,
1523 quote::MarketTradeDayRequest {
1524 market: market.to_string(),
1525 beg_day: format_date(begin),
1526 end_day: format_date(end),
1527 },
1528 )
1529 .await?;
1530 let trading_days = resp
1531 .trade_day
1532 .iter()
1533 .map(|value| {
1534 parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1535 })
1536 .collect::<Result<Vec<_>>>()?;
1537 let half_trading_days = resp
1538 .half_trade_day
1539 .iter()
1540 .map(|value| {
1541 parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1542 })
1543 .collect::<Result<Vec<_>>>()?;
1544 Ok(MarketTradingDays {
1545 trading_days,
1546 half_trading_days,
1547 })
1548 }
1549
1550 /// Get capital flow intraday
1551 ///
1552 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-flow-intraday>
1553 ///
1554 /// # Examples
1555 ///
1556 /// ```no_run
1557 /// use std::sync::Arc;
1558 ///
1559 /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1560 ///
1561 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1562 /// let oauth = OAuthBuilder::new("your-client-id")
1563 /// .build(|url| println!("Visit: {url}"))
1564 /// .await?;
1565 /// let config = Arc::new(Config::from_oauth(oauth));
1566 /// let (ctx, _) = QuoteContext::new(config);
1567 ///
1568 /// let resp = ctx.capital_flow("700.HK").await?;
1569 /// println!("{:?}", resp);
1570 /// # Ok::<_, Box<dyn std::error::Error>>(())
1571 /// # });
1572 pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1573 self.request::<_, quote::CapitalFlowIntradayResponse>(
1574 cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1575 quote::CapitalFlowIntradayRequest {
1576 symbol: symbol.into(),
1577 },
1578 )
1579 .await?
1580 .capital_flow_lines
1581 .into_iter()
1582 .map(TryInto::try_into)
1583 .collect()
1584 }
1585
1586 /// Get capital distribution
1587 ///
1588 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-distribution>
1589 ///
1590 /// # Examples
1591 ///
1592 /// ```no_run
1593 /// use std::sync::Arc;
1594 ///
1595 /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1596 ///
1597 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1598 /// let oauth = OAuthBuilder::new("your-client-id")
1599 /// .build(|url| println!("Visit: {url}"))
1600 /// .await?;
1601 /// let config = Arc::new(Config::from_oauth(oauth));
1602 /// let (ctx, _) = QuoteContext::new(config);
1603 ///
1604 /// let resp = ctx.capital_distribution("700.HK").await?;
1605 /// println!("{:?}", resp);
1606 /// # Ok::<_, Box<dyn std::error::Error>>(())
1607 /// # });
1608 pub async fn capital_distribution(
1609 &self,
1610 symbol: impl Into<String>,
1611 ) -> Result<CapitalDistributionResponse> {
1612 self.request::<_, quote::CapitalDistributionResponse>(
1613 cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1614 quote::SecurityRequest {
1615 symbol: symbol.into(),
1616 },
1617 )
1618 .await?
1619 .try_into()
1620 }
1621
1622 /// Get calc indexes
1623 pub async fn calc_indexes<I, T, J>(
1624 &self,
1625 symbols: I,
1626 indexes: J,
1627 ) -> Result<Vec<SecurityCalcIndex>>
1628 where
1629 I: IntoIterator<Item = T>,
1630 T: Into<String>,
1631 J: IntoIterator<Item = CalcIndex>,
1632 {
1633 let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1634 let resp: quote::SecurityCalcQuoteResponse = self
1635 .request(
1636 cmd_code::GET_CALC_INDEXES,
1637 quote::SecurityCalcQuoteRequest {
1638 symbols: symbols.into_iter().map(Into::into).collect(),
1639 calc_index: indexes
1640 .iter()
1641 .map(|i| quote::CalcIndex::from(*i).into())
1642 .collect(),
1643 },
1644 )
1645 .await?;
1646
1647 Ok(resp
1648 .security_calc_index
1649 .into_iter()
1650 .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1651 .collect())
1652 }
1653
1654 /// Get watchlist
1655 ///
1656 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_groups>
1657 ///
1658 /// # Examples
1659 ///
1660 /// ```no_run
1661 /// use std::sync::Arc;
1662 ///
1663 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1664 ///
1665 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1666 /// let oauth = OAuthBuilder::new("your-client-id")
1667 /// .build(|url| println!("Visit: {url}"))
1668 /// .await?;
1669 /// let config = Arc::new(Config::from_oauth(oauth));
1670 /// let (ctx, _) = QuoteContext::new(config);
1671 ///
1672 /// let resp = ctx.watchlist().await?;
1673 /// println!("{:?}", resp);
1674 /// # Ok::<_, Box<dyn std::error::Error>>(())
1675 /// # });
1676 /// ```
1677 pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1678 #[derive(Debug, Deserialize)]
1679 struct Response {
1680 groups: Vec<WatchlistGroup>,
1681 }
1682
1683 let resp = self
1684 .0
1685 .http_cli
1686 .request(Method::GET, "/v1/watchlist/groups")
1687 .response::<Json<Response>>()
1688 .send()
1689 .with_subscriber(self.0.log_subscriber.clone())
1690 .await?;
1691 Ok(resp.0.groups)
1692 }
1693
1694 /// Create watchlist group
1695 ///
1696 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_create_group>
1697 ///
1698 /// # Examples
1699 ///
1700 /// ```no_run
1701 /// use std::sync::Arc;
1702 ///
1703 /// use longbridge::{
1704 /// Config,
1705 /// oauth::OAuthBuilder,
1706 /// quote::{QuoteContext, RequestCreateWatchlistGroup},
1707 /// };
1708 ///
1709 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1710 /// let oauth = OAuthBuilder::new("your-client-id")
1711 /// .build(|url| println!("Visit: {url}"))
1712 /// .await?;
1713 /// let config = Arc::new(Config::from_oauth(oauth));
1714 /// let (ctx, _) = QuoteContext::new(config);
1715 ///
1716 /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1717 /// let group_id = ctx.create_watchlist_group(req).await?;
1718 /// println!("{}", group_id);
1719 /// # Ok::<_, Box<dyn std::error::Error>>(())
1720 /// # });
1721 /// ```
1722 pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1723 #[derive(Debug, Serialize)]
1724 struct RequestCreate {
1725 name: String,
1726 #[serde(skip_serializing_if = "Option::is_none")]
1727 securities: Option<Vec<String>>,
1728 }
1729
1730 #[derive(Debug, Deserialize)]
1731 struct Response {
1732 #[serde(with = "serde_utils::int64_str")]
1733 id: i64,
1734 }
1735
1736 let Json(Response { id }) = self
1737 .0
1738 .http_cli
1739 .request(Method::POST, "/v1/watchlist/groups")
1740 .body(Json(RequestCreate {
1741 name: req.name,
1742 securities: req.securities,
1743 }))
1744 .response::<Json<Response>>()
1745 .send()
1746 .with_subscriber(self.0.log_subscriber.clone())
1747 .await?;
1748
1749 Ok(id)
1750 }
1751
1752 /// Delete watchlist group
1753 ///
1754 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_delete_group>
1755 ///
1756 /// # Examples
1757 ///
1758 /// ```no_run
1759 /// use std::sync::Arc;
1760 ///
1761 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1762 ///
1763 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1764 /// let oauth = OAuthBuilder::new("your-client-id")
1765 /// .build(|url| println!("Visit: {url}"))
1766 /// .await?;
1767 /// let config = Arc::new(Config::from_oauth(oauth));
1768 /// let (ctx, _) = QuoteContext::new(config);
1769 ///
1770 /// ctx.delete_watchlist_group(10086, true).await?;
1771 /// # Ok::<_, Box<dyn std::error::Error>>(())
1772 /// # });
1773 /// ```
1774 pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1775 #[derive(Debug, Serialize)]
1776 struct Request {
1777 id: i64,
1778 purge: bool,
1779 }
1780
1781 Ok(self
1782 .0
1783 .http_cli
1784 .request(Method::DELETE, "/v1/watchlist/groups")
1785 .query_params(Request { id, purge })
1786 .send()
1787 .with_subscriber(self.0.log_subscriber.clone())
1788 .await?)
1789 }
1790
1791 /// Update watchlist group
1792 ///
1793 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group>
1794 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group_securities>
1795 ///
1796 /// # Examples
1797 ///
1798 /// ```no_run
1799 /// use std::sync::Arc;
1800 ///
1801 /// use longbridge::{
1802 /// Config,
1803 /// oauth::OAuthBuilder,
1804 /// quote::{QuoteContext, RequestUpdateWatchlistGroup},
1805 /// };
1806 ///
1807 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1808 /// let oauth = OAuthBuilder::new("your-client-id")
1809 /// .build(|url| println!("Visit: {url}"))
1810 /// .await?;
1811 /// let config = Arc::new(Config::from_oauth(oauth));
1812 /// let (ctx, _) = QuoteContext::new(config);
1813 /// let req = RequestUpdateWatchlistGroup::new(10086)
1814 /// .name("Watchlist2")
1815 /// .securities(["700.HK", "BABA.US"]);
1816 /// ctx.update_watchlist_group(req).await?;
1817 /// # Ok::<_, Box<dyn std::error::Error>>(())
1818 /// # });
1819 /// ```
1820 pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1821 #[derive(Debug, Serialize)]
1822 struct RequestUpdate {
1823 id: i64,
1824 #[serde(skip_serializing_if = "Option::is_none")]
1825 name: Option<String>,
1826 #[serde(skip_serializing_if = "Option::is_none")]
1827 securities: Option<Vec<String>>,
1828 #[serde(skip_serializing_if = "Option::is_none")]
1829 mode: Option<SecuritiesUpdateMode>,
1830 }
1831
1832 self.0
1833 .http_cli
1834 .request(Method::PUT, "/v1/watchlist/groups")
1835 .body(Json(RequestUpdate {
1836 id: req.id,
1837 name: req.name,
1838 mode: req.securities.is_some().then_some(req.mode),
1839 securities: req.securities,
1840 }))
1841 .send()
1842 .with_subscriber(self.0.log_subscriber.clone())
1843 .await?;
1844
1845 Ok(())
1846 }
1847
1848 /// Get security list
1849 pub async fn security_list(
1850 &self,
1851 market: Market,
1852 category: impl Into<Option<SecurityListCategory>>,
1853 ) -> Result<Vec<Security>> {
1854 #[derive(Debug, Serialize)]
1855 struct Request {
1856 market: Market,
1857 #[serde(skip_serializing_if = "Option::is_none")]
1858 category: Option<SecurityListCategory>,
1859 }
1860
1861 #[derive(Debug, Deserialize)]
1862 struct Response {
1863 list: Vec<Security>,
1864 }
1865
1866 Ok(self
1867 .0
1868 .http_cli
1869 .request(Method::GET, "/v1/quote/get_security_list")
1870 .query_params(Request {
1871 market,
1872 category: category.into(),
1873 })
1874 .response::<Json<Response>>()
1875 .send()
1876 .with_subscriber(self.0.log_subscriber.clone())
1877 .await?
1878 .0
1879 .list)
1880 }
1881
1882 /// Get filings list
1883 pub async fn filings(&self, symbol: impl Into<String>) -> Result<Vec<FilingItem>> {
1884 #[derive(Debug, Serialize)]
1885 struct Request {
1886 symbol: String,
1887 }
1888
1889 #[derive(Debug, Deserialize)]
1890 struct Response {
1891 items: Vec<FilingItem>,
1892 }
1893
1894 Ok(self
1895 .0
1896 .http_cli
1897 .request(Method::GET, "/v1/quote/filings")
1898 .query_params(Request {
1899 symbol: symbol.into(),
1900 })
1901 .response::<Json<Response>>()
1902 .send()
1903 .with_subscriber(self.0.log_subscriber.clone())
1904 .await?
1905 .0
1906 .items)
1907 }
1908
1909 /// Get current market temperature
1910 ///
1911 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/market_temperature>
1912 ///
1913 /// # Examples
1914 ///
1915 /// ```no_run
1916 /// use std::sync::Arc;
1917 ///
1918 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1919 ///
1920 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1921 /// let oauth = OAuthBuilder::new("your-client-id")
1922 /// .build(|url| println!("Visit: {url}"))
1923 /// .await?;
1924 /// let config = Arc::new(Config::from_oauth(oauth));
1925 /// let (ctx, _) = QuoteContext::new(config);
1926 ///
1927 /// let resp = ctx.market_temperature(Market::HK).await?;
1928 /// println!("{:?}", resp);
1929 /// # Ok::<_, Box<dyn std::error::Error>>(())
1930 /// # });
1931 /// ```
1932 pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1933 #[derive(Debug, Serialize)]
1934 struct Request {
1935 market: Market,
1936 }
1937
1938 Ok(self
1939 .0
1940 .http_cli
1941 .request(Method::GET, "/v1/quote/market_temperature")
1942 .query_params(Request { market })
1943 .response::<Json<MarketTemperature>>()
1944 .send()
1945 .with_subscriber(self.0.log_subscriber.clone())
1946 .await?
1947 .0)
1948 }
1949
1950 /// Get historical market temperature
1951 ///
1952 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/history_market_temperature>
1953 ///
1954 /// # Examples
1955 ///
1956 /// ```no_run
1957 /// use std::sync::Arc;
1958 ///
1959 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1960 /// use time::macros::date;
1961 ///
1962 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1963 /// let oauth = OAuthBuilder::new("your-client-id")
1964 /// .build(|url| println!("Visit: {url}"))
1965 /// .await?;
1966 /// let config = Arc::new(Config::from_oauth(oauth));
1967 /// let (ctx, _) = QuoteContext::new(config);
1968 ///
1969 /// let resp = ctx
1970 /// .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1971 /// .await?;
1972 /// println!("{:?}", resp);
1973 /// # Ok::<_, Box<dyn std::error::Error>>(())
1974 /// # });
1975 /// ```
1976 pub async fn history_market_temperature(
1977 &self,
1978 market: Market,
1979 start_date: Date,
1980 end_date: Date,
1981 ) -> Result<HistoryMarketTemperatureResponse> {
1982 #[derive(Debug, Serialize)]
1983 struct Request {
1984 market: Market,
1985 start_date: String,
1986 end_date: String,
1987 }
1988
1989 Ok(self
1990 .0
1991 .http_cli
1992 .request(Method::GET, "/v1/quote/history_market_temperature")
1993 .query_params(Request {
1994 market,
1995 start_date: format_date(start_date),
1996 end_date: format_date(end_date),
1997 })
1998 .response::<Json<HistoryMarketTemperatureResponse>>()
1999 .send()
2000 .with_subscriber(self.0.log_subscriber.clone())
2001 .await?
2002 .0)
2003 }
2004
2005 /// Get real-time quotes
2006 ///
2007 /// Get real-time quotes of the subscribed symbols, it always returns the
2008 /// data in the local storage.
2009 ///
2010 /// # Examples
2011 ///
2012 /// ```no_run
2013 /// use std::{sync::Arc, time::Duration};
2014 ///
2015 /// use longbridge::{
2016 /// Config,
2017 /// oauth::OAuthBuilder,
2018 /// quote::{QuoteContext, SubFlags},
2019 /// };
2020 ///
2021 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
2022 /// let oauth = OAuthBuilder::new("your-client-id")
2023 /// .build(|url| println!("Visit: {url}"))
2024 /// .await?;
2025 /// let config = Arc::new(Config::from_oauth(oauth));
2026 /// let (ctx, _) = QuoteContext::new(config);
2027 ///
2028 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
2029 /// .await?;
2030 /// tokio::time::sleep(Duration::from_secs(5)).await;
2031 ///
2032 /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
2033 /// println!("{:?}", resp);
2034 /// # Ok::<_, Box<dyn std::error::Error>>(())
2035 /// # });
2036 /// ```
2037 pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
2038 where
2039 I: IntoIterator<Item = T>,
2040 T: Into<String>,
2041 {
2042 let (reply_tx, reply_rx) = oneshot::channel();
2043 self.0
2044 .command_tx
2045 .send(Command::GetRealtimeQuote {
2046 symbols: symbols.into_iter().map(Into::into).collect(),
2047 reply_tx,
2048 })
2049 .map_err(|_| WsClientError::ClientClosed)?;
2050 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
2051 }
2052
2053 /// Get real-time depth
2054 ///
2055 /// Get real-time depth of the subscribed symbols, it always returns the
2056 /// data in the local storage.
2057 ///
2058 /// # Examples
2059 ///
2060 /// ```no_run
2061 /// use std::{sync::Arc, time::Duration};
2062 ///
2063 /// use longbridge::{
2064 /// Config,
2065 /// oauth::OAuthBuilder,
2066 /// quote::{QuoteContext, SubFlags},
2067 /// };
2068 ///
2069 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
2070 /// let oauth = OAuthBuilder::new("your-client-id")
2071 /// .build(|url| println!("Visit: {url}"))
2072 /// .await?;
2073 /// let config = Arc::new(Config::from_oauth(oauth));
2074 /// let (ctx, _) = QuoteContext::new(config);
2075 ///
2076 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH)
2077 /// .await?;
2078 /// tokio::time::sleep(Duration::from_secs(5)).await;
2079 ///
2080 /// let resp = ctx.realtime_depth("700.HK").await?;
2081 /// println!("{:?}", resp);
2082 /// # Ok::<_, Box<dyn std::error::Error>>(())
2083 /// # });
2084 /// ```
2085 pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
2086 let (reply_tx, reply_rx) = oneshot::channel();
2087 self.0
2088 .command_tx
2089 .send(Command::GetRealtimeDepth {
2090 symbol: symbol.into(),
2091 reply_tx,
2092 })
2093 .map_err(|_| WsClientError::ClientClosed)?;
2094 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
2095 }
2096
2097 /// Get real-time trades
2098 ///
2099 /// Get real-time trades of the subscribed symbols, it always returns the
2100 /// data in the local storage.
2101 ///
2102 /// # Examples
2103 ///
2104 /// ```no_run
2105 /// use std::{sync::Arc, time::Duration};
2106 ///
2107 /// use longbridge::{
2108 /// Config,
2109 /// oauth::OAuthBuilder,
2110 /// quote::{QuoteContext, SubFlags},
2111 /// };
2112 ///
2113 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
2114 /// let oauth = OAuthBuilder::new("your-client-id")
2115 /// .build(|url| println!("Visit: {url}"))
2116 /// .await?;
2117 /// let config = Arc::new(Config::from_oauth(oauth));
2118 /// let (ctx, _) = QuoteContext::new(config);
2119 ///
2120 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE)
2121 /// .await?;
2122 /// tokio::time::sleep(Duration::from_secs(5)).await;
2123 ///
2124 /// let resp = ctx.realtime_trades("700.HK", 10).await?;
2125 /// println!("{:?}", resp);
2126 /// # Ok::<_, Box<dyn std::error::Error>>(())
2127 /// # });
2128 /// ```
2129 pub async fn realtime_trades(
2130 &self,
2131 symbol: impl Into<String>,
2132 count: usize,
2133 ) -> Result<Vec<Trade>> {
2134 let (reply_tx, reply_rx) = oneshot::channel();
2135 self.0
2136 .command_tx
2137 .send(Command::GetRealtimeTrade {
2138 symbol: symbol.into(),
2139 count,
2140 reply_tx,
2141 })
2142 .map_err(|_| WsClientError::ClientClosed)?;
2143 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
2144 }
2145
2146 /// Get real-time broker queue
2147 ///
2148 ///
2149 /// Get real-time broker queue of the subscribed symbols, it always returns
2150 /// the data in the local storage.
2151 ///
2152 /// # Examples
2153 ///
2154 /// ```no_run
2155 /// use std::{sync::Arc, time::Duration};
2156 ///
2157 /// use longbridge::{
2158 /// Config,
2159 /// oauth::OAuthBuilder,
2160 /// quote::{QuoteContext, SubFlags},
2161 /// };
2162 ///
2163 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
2164 /// let oauth = OAuthBuilder::new("your-client-id")
2165 /// .build(|url| println!("Visit: {url}"))
2166 /// .await?;
2167 /// let config = Arc::new(Config::from_oauth(oauth));
2168 /// let (ctx, _) = QuoteContext::new(config);
2169 ///
2170 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER)
2171 /// .await?;
2172 /// tokio::time::sleep(Duration::from_secs(5)).await;
2173 ///
2174 /// let resp = ctx.realtime_brokers("700.HK").await?;
2175 /// println!("{:?}", resp);
2176 /// # Ok::<_, Box<dyn std::error::Error>>(())
2177 /// # });
2178 /// ```
2179 pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
2180 let (reply_tx, reply_rx) = oneshot::channel();
2181 self.0
2182 .command_tx
2183 .send(Command::GetRealtimeBrokers {
2184 symbol: symbol.into(),
2185 reply_tx,
2186 })
2187 .map_err(|_| WsClientError::ClientClosed)?;
2188 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
2189 }
2190
2191 /// Get real-time candlesticks
2192 ///
2193 /// Get real-time candlesticks of the subscribed symbols, it always returns
2194 /// the data in the local storage.
2195 ///
2196 /// # Examples
2197 ///
2198 /// ```no_run
2199 /// use std::{sync::Arc, time::Duration};
2200 ///
2201 /// use longbridge::{
2202 /// Config,
2203 /// oauth::OAuthBuilder,
2204 /// quote::{Period, QuoteContext, TradeSessions},
2205 /// };
2206 ///
2207 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
2208 /// let oauth = OAuthBuilder::new("your-client-id")
2209 /// .build(|url| println!("Visit: {url}"))
2210 /// .await?;
2211 /// let config = Arc::new(Config::from_oauth(oauth));
2212 /// let (ctx, _) = QuoteContext::new(config);
2213 ///
2214 /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
2215 /// .await?;
2216 /// tokio::time::sleep(Duration::from_secs(5)).await;
2217 ///
2218 /// let resp = ctx
2219 /// .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
2220 /// .await?;
2221 /// println!("{:?}", resp);
2222 /// # Ok::<_, Box<dyn std::error::Error>>(())
2223 /// # });
2224 /// ```
2225 pub async fn realtime_candlesticks(
2226 &self,
2227 symbol: impl Into<String>,
2228 period: Period,
2229 count: usize,
2230 ) -> Result<Vec<Candlestick>> {
2231 let (reply_tx, reply_rx) = oneshot::channel();
2232 self.0
2233 .command_tx
2234 .send(Command::GetRealtimeCandlesticks {
2235 symbol: symbol.into(),
2236 period,
2237 count,
2238 reply_tx,
2239 })
2240 .map_err(|_| WsClientError::ClientClosed)?;
2241 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
2242 }
2243
2244 // ── short_positions ───────────────────────────────────────────
2245
2246 /// Get short interest data for a US or HK security.
2247 ///
2248 /// Market is inferred from the symbol suffix:
2249 /// - `.HK` → `GET /v1/quote/short-positions/hk`
2250 /// - otherwise → `GET /v1/quote/short-positions/us`
2251 ///
2252 /// `count` controls the number of records returned (1–100, default 20).
2253 pub async fn short_positions(
2254 &self,
2255 symbol: impl Into<String>,
2256 count: u32,
2257 ) -> Result<ShortPositionsResponse> {
2258 use std::time::{SystemTime, UNIX_EPOCH};
2259
2260 let sym = symbol.into();
2261 let is_hk = sym.to_uppercase().ends_with(".HK");
2262 let path = if is_hk {
2263 "/v1/quote/short-positions/hk"
2264 } else {
2265 "/v1/quote/short-positions/us"
2266 };
2267 let ts = SystemTime::now()
2268 .duration_since(UNIX_EPOCH)
2269 .map(|d| d.as_secs())
2270 .unwrap_or(0);
2271
2272 #[derive(serde::Serialize)]
2273 struct Query {
2274 symbol: String,
2275 last_timestamp: String,
2276 count: u32,
2277 }
2278 // Response: {"symbol":"AAPL.US","data":[{...}]} — only `data` is used.
2279 let outer: serde_json::Value = self
2280 .0
2281 .http_cli
2282 .request(Method::GET, path)
2283 .query_params(Query {
2284 symbol: sym.clone(),
2285 last_timestamp: ts.to_string(),
2286 count,
2287 })
2288 .response::<Json<serde_json::Value>>()
2289 .send()
2290 .with_subscriber(self.0.log_subscriber.clone())
2291 .await?
2292 .0;
2293 let empty = vec![];
2294 let raw = outer["data"].as_array().unwrap_or(&empty);
2295 let data = raw
2296 .iter()
2297 .map(|v| {
2298 let ts_str = v["timestamp"].as_str().unwrap_or("").to_string();
2299 ShortPositionsItem {
2300 timestamp: unix_secs_to_rfc3339(&ts_str),
2301 rate: v["rate"].as_str().unwrap_or("").to_string(),
2302 close: v["close"].as_str().unwrap_or("").to_string(),
2303 current_shares_short: v["current_shares_short"]
2304 .as_str()
2305 .unwrap_or("")
2306 .to_string(),
2307 avg_daily_share_volume: v["avg_daily_share_volume"]
2308 .as_str()
2309 .unwrap_or("")
2310 .to_string(),
2311 days_to_cover: v["days_to_cover"].as_str().unwrap_or("").to_string(),
2312 amount: v["amount"].as_str().unwrap_or("").to_string(),
2313 balance: v["balance"].as_str().unwrap_or("").to_string(),
2314 cost: v["cost"].as_str().unwrap_or("").to_string(),
2315 }
2316 })
2317 .collect();
2318 Ok(ShortPositionsResponse { data })
2319 }
2320
2321 // ── option_volume ─────────────────────────────────────────────
2322
2323 /// Get real-time option call/put volume for a security.
2324 ///
2325 /// Path: `GET /v1/quote/option-volume-stats`
2326 pub async fn option_volume(&self, symbol: impl Into<String>) -> Result<OptionVolumeStats> {
2327 #[derive(serde::Serialize)]
2328 struct Query {
2329 symbol: String,
2330 }
2331 #[derive(serde::Deserialize)]
2332 struct RawOptionVolumeStats {
2333 c: String,
2334 p: String,
2335 }
2336 let symbol = symbol.into();
2337 let resp = self
2338 .0
2339 .http_cli
2340 .request(Method::GET, "/v1/quote/option-volume-stats")
2341 .query_params(Query {
2342 symbol: symbol.clone(),
2343 })
2344 .response::<Json<RawOptionVolumeStats>>()
2345 .send()
2346 .with_subscriber(self.0.log_subscriber.clone())
2347 .await?;
2348 let raw = resp.0;
2349 Ok(OptionVolumeStats {
2350 symbol,
2351 call_volume: raw.c.parse().unwrap_or(0),
2352 put_volume: raw.p.parse().unwrap_or(0),
2353 })
2354 }
2355
2356 /// Get daily historical option volume for a security.
2357 ///
2358 /// Path: `GET /v1/quote/option-volume-stats/daily`
2359 pub async fn option_volume_daily(
2360 &self,
2361 symbol: impl Into<String>,
2362 timestamp: i64,
2363 count: u32,
2364 ) -> Result<OptionVolumeDaily> {
2365 #[derive(serde::Serialize)]
2366 struct Query {
2367 symbol: String,
2368 timestamp: i64,
2369 line_num: u32,
2370 direction: i32,
2371 }
2372 #[derive(serde::Deserialize)]
2373 struct RawDailyStat {
2374 symbol: String,
2375 timestamp: String,
2376 total_call_volume: String,
2377 total_put_volume: String,
2378 total_call_open_interest: String,
2379 total_put_open_interest: String,
2380 total_volume: String,
2381 total_open_interest: String,
2382 #[serde(deserialize_with = "crate::serde_utils::f64_str::deserialize")]
2383 put_call_volume_ratio: f64,
2384 #[serde(deserialize_with = "crate::serde_utils::f64_str::deserialize")]
2385 put_call_open_interest_ratio: f64,
2386 }
2387 #[derive(serde::Deserialize)]
2388 struct RawOptionVolumeDaily {
2389 stats: Vec<RawDailyStat>,
2390 }
2391 let symbol = symbol.into();
2392 let resp = self
2393 .0
2394 .http_cli
2395 .request(Method::GET, "/v1/quote/option-volume-stats/daily")
2396 .query_params(Query {
2397 symbol: symbol.clone(),
2398 timestamp,
2399 line_num: count,
2400 direction: 1,
2401 })
2402 .response::<Json<RawOptionVolumeDaily>>()
2403 .send()
2404 .with_subscriber(self.0.log_subscriber.clone())
2405 .await?;
2406 let raw = resp.0;
2407 let stats = raw
2408 .stats
2409 .into_iter()
2410 .map(|item| {
2411 let ts: i64 = item.timestamp.parse().unwrap_or(0);
2412 OptionVolumeDailyStat {
2413 symbol: item.symbol,
2414 date: time::OffsetDateTime::from_unix_timestamp(ts)
2415 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
2416 .date(),
2417 call_volume: item.total_call_volume.parse().unwrap_or(0),
2418 put_volume: item.total_put_volume.parse().unwrap_or(0),
2419 call_open_interest: item.total_call_open_interest.parse().unwrap_or(0),
2420 put_open_interest: item.total_put_open_interest.parse().unwrap_or(0),
2421 total_volume: item.total_volume.parse().unwrap_or(0),
2422 total_open_interest: item.total_open_interest.parse().unwrap_or(0),
2423 pc_vol: item.put_call_volume_ratio,
2424 pc_oi: item.put_call_open_interest_ratio,
2425 }
2426 })
2427 .collect();
2428 Ok(OptionVolumeDaily { symbol, stats })
2429 }
2430 // ── short_trades ──────────────────────────────────────────────
2431
2432 /// Get short trade records for a HK or US security.
2433 ///
2434 /// The API endpoint is auto-detected from the symbol suffix:
2435 /// `.HK` → `GET /v1/quote/short-trades/hk`,
2436 /// otherwise → `GET /v1/quote/short-trades/us`.
2437 pub async fn short_trades(
2438 &self,
2439 symbol: impl Into<String>,
2440 count: u32,
2441 ) -> Result<ShortTradesResponse> {
2442 use std::time::{SystemTime, UNIX_EPOCH};
2443
2444 #[derive(serde::Serialize)]
2445 struct Query {
2446 symbol: String,
2447 last_timestamp: String,
2448 page_size: String,
2449 }
2450 let sym = symbol.into();
2451 let path = if sym.to_uppercase().ends_with(".HK") {
2452 "/v1/quote/short-trades/hk"
2453 } else {
2454 "/v1/quote/short-trades/us"
2455 };
2456 let ts = SystemTime::now()
2457 .duration_since(UNIX_EPOCH)
2458 .map(|d| d.as_secs())
2459 .unwrap_or(0);
2460 // Response: {"symbol":"00700.HK","data":[{...}]} — only `data` is used.
2461 let outer: serde_json::Value = self
2462 .0
2463 .http_cli
2464 .request(Method::GET, path)
2465 .query_params(Query {
2466 symbol: sym.clone(),
2467 last_timestamp: ts.to_string(),
2468 page_size: count.to_string(),
2469 })
2470 .response::<Json<serde_json::Value>>()
2471 .send()
2472 .with_subscriber(self.0.log_subscriber.clone())
2473 .await?
2474 .0;
2475 let empty = vec![];
2476 let raw = outer["data"].as_array().unwrap_or(&empty);
2477 let data = raw
2478 .iter()
2479 .map(|v| {
2480 let ts_str = v["timestamp"].as_str().unwrap_or("").to_string();
2481 ShortTradesItem {
2482 timestamp: unix_secs_to_rfc3339(&ts_str),
2483 rate: v["rate"].as_str().unwrap_or("").to_string(),
2484 close: v["close"].as_str().unwrap_or("").to_string(),
2485 nus_amount: v["nus_amount"].as_str().unwrap_or("").to_string(),
2486 ny_amount: v["ny_amount"].as_str().unwrap_or("").to_string(),
2487 total_amount: v["total_amount"].as_str().unwrap_or("").to_string(),
2488 amount: v["amount"].as_str().unwrap_or("").to_string(),
2489 balance: v["balance"].as_str().unwrap_or("").to_string(),
2490 }
2491 })
2492 .collect();
2493 Ok(ShortTradesResponse { data })
2494 }
2495
2496 // ── update_pinned ─────────────────────────────────────────────
2497
2498 /// Pin or unpin watchlist securities.
2499 ///
2500 /// Path: `POST /v1/watchlist/pinned`
2501 pub async fn update_pinned(&self, mode: PinnedMode, symbols: Vec<String>) -> Result<()> {
2502 #[derive(Debug, Serialize)]
2503 struct Request {
2504 mode: PinnedMode,
2505 securities: Vec<String>,
2506 }
2507
2508 self.0
2509 .http_cli
2510 .request(Method::POST, "/v1/watchlist/pinned")
2511 .body(Json(Request {
2512 mode,
2513 securities: symbols,
2514 }))
2515 .send()
2516 .with_subscriber(self.0.log_subscriber.clone())
2517 .await?;
2518
2519 Ok(())
2520 }
2521
2522 // ── US-market APIs
2523 // ────────────────────────────────────────────────────────
2524
2525 /// Get cryptocurrency market overview.
2526 ///
2527 /// `symbol` must be in `PAIR.EXCHANGE` format.
2528 /// US DC uses the **BKKT** exchange: e.g. `"BTCUSD.BKKT"` →
2529 /// `"VA/BKKT/BTCUSD"`. Pass the exchange suffix explicitly.
2530 ///
2531 /// Path: `GET /v1/us/gemini/crypto-overview`
2532 ///
2533 /// US token required.
2534 pub async fn us_crypto_overview(
2535 &self,
2536 symbol: impl Into<String>,
2537 ) -> Result<crate::quote::USCryptoOverview> {
2538 #[derive(Serialize)]
2539 struct Query {
2540 symbol: String,
2541 }
2542 Ok(self
2543 .0
2544 .http_cli
2545 .request(Method::GET, "/v1/us/gemini/crypto-overview")
2546 .dc_restrict(DcRegion::Us)
2547 .query_params(Query {
2548 symbol: symbol.into(),
2549 })
2550 .response::<Json<crate::quote::USCryptoOverview>>()
2551 .send()
2552 .with_subscriber(self.0.log_subscriber.clone())
2553 .await?
2554 .0)
2555 }
2556}
2557
2558fn normalize_symbol(symbol: &str) -> &str {
2559 match symbol.split_once('.') {
2560 Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
2561 _ => symbol,
2562 }
2563}
2564
2565#[cfg(test)]
2566mod http_request_tests {
2567 use serde_json::json;
2568 use time::macros::datetime;
2569
2570 use super::*;
2571
2572 /// Locks the JSON shape the HTTP transport sends for a top-up offset
2573 /// query: proto field names, numeric enums, New York wall-clock
2574 /// `date`/`minute`, and no `date_request` key.
2575 #[test]
2576 fn history_offset_request_json() {
2577 let req = history_offset_request(
2578 "AAPL.US".to_string(),
2579 Period::OneMinute,
2580 AdjustType::NoAdjust,
2581 false,
2582 Some(datetime!(2026-10-09 03:59)),
2583 16,
2584 TradeSessions::All,
2585 );
2586 let mut v = serde_json::to_value(&req).unwrap();
2587 http_json::strip_nulls(&mut v);
2588 assert_eq!(
2589 v,
2590 json!({
2591 "symbol": "AAPL.US",
2592 "period": quote::Period::OneMinute as i32,
2593 "adjust_type": quote::AdjustType::NoAdjust as i32,
2594 "query_type": quote::HistoryCandlestickQueryType::QueryByOffset as i32,
2595 "offset_request": {
2596 "direction": quote::Direction::Backward as i32,
2597 "date": "20261009",
2598 "minute": "0359",
2599 "count": 16,
2600 },
2601 "trade_session": 100,
2602 })
2603 );
2604 }
2605
2606 /// Request bodies for a few other mapped commands keep proto names, and
2607 /// the warrant filter's `r#type` is sent as `type`.
2608 #[test]
2609 fn request_json_shapes() {
2610 let v = serde_json::to_value(quote::MultiSecurityRequest {
2611 symbol: vec!["700.HK".into()],
2612 })
2613 .unwrap();
2614 assert_eq!(v, json!({ "symbol": ["700.HK"] }));
2615
2616 let v = serde_json::to_value(quote::SecurityIntradayRequest {
2617 symbol: "AAPL.US".into(),
2618 trade_session: TradeSessions::All as i32,
2619 })
2620 .unwrap();
2621 assert_eq!(v, json!({ "symbol": "AAPL.US", "trade_session": 100 }));
2622
2623 let v = serde_json::to_value(quote::FilterConfig {
2624 r#type: vec![0],
2625 ..Default::default()
2626 })
2627 .unwrap();
2628 assert_eq!(v["type"], json!([0]));
2629 }
2630}