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