Skip to main content

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(&quote.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}