longbridge/quote/
context.rs

1use std::{
2    sync::{Arc, RwLock},
3    time::Duration,
4};
5
6use longbridge_httpcli::{HttpClient, Json, Method};
7use longbridge_proto::quote;
8use longbridge_wscli::WsClientError;
9use serde::{Deserialize, Serialize};
10use time::{Date, PrimitiveDateTime};
11use tokio::sync::{mpsc, oneshot};
12use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
13
14use crate::{
15    Config, Error, Language, Market, Result,
16    quote::{
17        AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine,
18        FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature,
19        MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, PushEvent,
20        QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup,
21        RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth,
22        SecurityListCategory, SecurityQuote, SecurityStaticInfo, StrikePriceInfo, Subscription,
23        Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup,
24        cache::{Cache, CacheWithKey},
25        cmd_code,
26        core::{Command, Core, UserProfile},
27        sub_flags::SubFlags,
28        types::{
29            FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, SecuritiesUpdateMode,
30            SortOrderType, WarrantSortBy, WarrantStatus,
31        },
32        utils::{format_date, parse_date},
33    },
34    serde_utils,
35};
36
37const RETRY_COUNT: usize = 3;
38const PARTICIPANT_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
39const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
40const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
41const OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
42const TRADING_SESSION_CACHE_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2);
43
44struct InnerQuoteContext {
45    language: Language,
46    http_cli: HttpClient,
47    command_tx: mpsc::UnboundedSender<Command>,
48    cache_participants: Cache<Vec<ParticipantInfo>>,
49    cache_issuers: Cache<Vec<IssuerInfo>>,
50    cache_option_chain_expiry_date_list: CacheWithKey<String, Vec<Date>>,
51    cache_option_chain_strike_info: CacheWithKey<(String, Date), Vec<StrikePriceInfo>>,
52    cache_trading_session: Cache<Vec<MarketTradingSession>>,
53    user_profile: Arc<RwLock<Option<UserProfile>>>,
54    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
55}
56
57impl Drop for InnerQuoteContext {
58    fn drop(&mut self) {
59        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
60            tracing::info!("quote context dropped");
61        });
62    }
63}
64
65/// Quote context
66#[derive(Clone)]
67pub struct QuoteContext(Arc<InnerQuoteContext>);
68
69impl QuoteContext {
70    /// Create a `QuoteContext`
71    pub fn new(config: Arc<Config>) -> (Self, mpsc::UnboundedReceiver<PushEvent>) {
72        let log_subscriber = config.create_log_subscriber("quote");
73
74        dispatcher::with_default(&log_subscriber.clone().into(), || {
75            tracing::info!(
76                language = ?config.language,
77                enable_overnight = ?config.enable_overnight,
78                push_candlestick_mode = ?config.push_candlestick_mode,
79                enable_print_quote_packages = ?config.enable_print_quote_packages,
80                "creating quote context"
81            );
82        });
83
84        let language = config.language;
85        let http_cli = config.create_http_client();
86        let (command_tx, command_rx) = mpsc::unbounded_channel();
87        let (push_tx, push_rx) = mpsc::unbounded_channel();
88        let user_profile = Arc::new(RwLock::new(None::<UserProfile>));
89        let core = Core::new(config, command_rx, push_tx, user_profile.clone());
90        crate::runtime::RUNTIME
91            .handle()
92            .spawn(core.run().with_subscriber(log_subscriber.clone()));
93
94        dispatcher::with_default(&log_subscriber.clone().into(), || {
95            tracing::info!("quote context created");
96        });
97
98        (
99            QuoteContext(Arc::new(InnerQuoteContext {
100                language,
101                http_cli,
102                command_tx,
103                cache_participants: Cache::new(PARTICIPANT_INFO_CACHE_TIMEOUT),
104                cache_issuers: Cache::new(ISSUER_INFO_CACHE_TIMEOUT),
105                cache_option_chain_expiry_date_list: CacheWithKey::new(
106                    OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT,
107                ),
108                cache_option_chain_strike_info: CacheWithKey::new(
109                    OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT,
110                ),
111                cache_trading_session: Cache::new(TRADING_SESSION_CACHE_TIMEOUT),
112                user_profile,
113                log_subscriber,
114            })),
115            push_rx,
116        )
117    }
118
119    /// Returns the log subscriber
120    #[inline]
121    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
122        self.0.log_subscriber.clone()
123    }
124
125    async fn ensure_user_profile(&self) -> Result<()> {
126        if self.0.user_profile.read().unwrap().is_some() {
127            return Ok(());
128        }
129        let (reply_tx, reply_rx) = oneshot::channel();
130        self.0
131            .command_tx
132            .send(Command::EnsureConnected { reply_tx })
133            .map_err(|_| WsClientError::ClientClosed)?;
134        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
135    }
136
137    /// Returns the member ID
138    pub async fn member_id(&self) -> Result<i64> {
139        self.ensure_user_profile().await?;
140        Ok(self
141            .0
142            .user_profile
143            .read()
144            .unwrap()
145            .as_ref()
146            .unwrap()
147            .member_id)
148    }
149
150    /// Returns the quote level
151    pub async fn quote_level(&self) -> Result<String> {
152        self.ensure_user_profile().await?;
153        Ok(self
154            .0
155            .user_profile
156            .read()
157            .unwrap()
158            .as_ref()
159            .unwrap()
160            .quote_level
161            .clone())
162    }
163
164    /// Returns the quote package details
165    pub async fn quote_package_details(&self) -> Result<Vec<QuotePackageDetail>> {
166        self.ensure_user_profile().await?;
167        Ok(self
168            .0
169            .user_profile
170            .read()
171            .unwrap()
172            .as_ref()
173            .unwrap()
174            .quote_package_details
175            .clone())
176    }
177
178    /// Send a raw request
179    async fn request_raw(&self, command_code: u8, body: Vec<u8>) -> Result<Vec<u8>> {
180        for _ in 0..RETRY_COUNT {
181            let (reply_tx, reply_rx) = oneshot::channel();
182            self.0
183                .command_tx
184                .send(Command::Request {
185                    command_code,
186                    body: body.clone(),
187                    reply_tx,
188                })
189                .map_err(|_| WsClientError::ClientClosed)?;
190            let res = reply_rx.await.map_err(|_| WsClientError::ClientClosed)?;
191
192            match res {
193                Ok(resp) => return Ok(resp),
194                Err(Error::WsClient(WsClientError::Cancelled)) => {}
195                Err(err) => return Err(err),
196            }
197        }
198
199        Err(Error::WsClient(WsClientError::RequestTimeout))
200    }
201
202    /// Send a request `T` to get a response `R`
203    async fn request<T, R>(&self, command_code: u8, req: T) -> Result<R>
204    where
205        T: prost::Message,
206        R: prost::Message + Default,
207    {
208        let resp = self.request_raw(command_code, req.encode_to_vec()).await?;
209        Ok(R::decode(&*resp)?)
210    }
211
212    /// Send a request to get a response `R`
213    async fn request_without_body<R>(&self, command_code: u8) -> Result<R>
214    where
215        R: prost::Message + Default,
216    {
217        let resp = self.request_raw(command_code, vec![]).await?;
218        Ok(R::decode(&*resp)?)
219    }
220
221    /// Subscribe
222    ///
223    /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/subscribe>
224    ///
225    /// # Examples
226    ///
227    /// ```no_run
228    /// use std::sync::Arc;
229    ///
230    /// use longbridge::{
231    ///     Config,
232    ///     oauth::OAuthBuilder,
233    ///     quote::{QuoteContext, SubFlags},
234    /// };
235    ///
236    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
237    /// let oauth = OAuthBuilder::new("your-client-id")
238    ///     .build(|url| println!("Visit: {url}"))
239    ///     .await?;
240    /// let config = Arc::new(Config::from_oauth(oauth));
241    /// let (ctx, mut receiver) = QuoteContext::new(config);
242    ///
243    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
244    ///     .await?;
245    /// while let Some(msg) = receiver.recv().await {
246    ///     println!("{:?}", msg);
247    /// }
248    /// # Ok::<_, Box<dyn std::error::Error>>(())
249    /// # });
250    /// ```
251    pub async fn subscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
252    where
253        I: IntoIterator<Item = T>,
254        T: AsRef<str>,
255    {
256        let (reply_tx, reply_rx) = oneshot::channel();
257        self.0
258            .command_tx
259            .send(Command::Subscribe {
260                symbols: symbols
261                    .into_iter()
262                    .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
263                    .collect(),
264                sub_types: sub_types.into(),
265                reply_tx,
266            })
267            .map_err(|_| WsClientError::ClientClosed)?;
268        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
269    }
270
271    /// Unsubscribe
272    ///
273    /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/unsubscribe>
274    ///
275    /// # Examples
276    ///
277    /// ```no_run
278    /// use std::sync::Arc;
279    ///
280    /// use longbridge::{
281    ///     Config,
282    ///     oauth::OAuthBuilder,
283    ///     quote::{QuoteContext, SubFlags},
284    /// };
285    ///
286    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
287    /// let oauth = OAuthBuilder::new("your-client-id")
288    ///     .build(|url| println!("Visit: {url}"))
289    ///     .await?;
290    /// let config = Arc::new(Config::from_oauth(oauth));
291    /// let (ctx, _) = QuoteContext::new(config);
292    ///
293    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
294    ///     .await?;
295    /// ctx.unsubscribe(["AAPL.US"], SubFlags::QUOTE).await?;
296    /// # Ok::<_, Box<dyn std::error::Error>>(())
297    /// # });
298    /// ```
299    pub async fn unsubscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
300    where
301        I: IntoIterator<Item = T>,
302        T: AsRef<str>,
303    {
304        let (reply_tx, reply_rx) = oneshot::channel();
305        self.0
306            .command_tx
307            .send(Command::Unsubscribe {
308                symbols: symbols
309                    .into_iter()
310                    .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
311                    .collect(),
312                sub_types: sub_types.into(),
313                reply_tx,
314            })
315            .map_err(|_| WsClientError::ClientClosed)?;
316        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
317    }
318
319    /// Subscribe security candlesticks
320    ///
321    /// # Examples
322    ///
323    /// ```no_run
324    /// use std::sync::Arc;
325    ///
326    /// use longbridge::{
327    ///     Config,
328    ///     oauth::OAuthBuilder,
329    ///     quote::{Period, QuoteContext, TradeSessions},
330    /// };
331    ///
332    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
333    /// let oauth = OAuthBuilder::new("your-client-id")
334    ///     .build(|url| println!("Visit: {url}"))
335    ///     .await?;
336    /// let config = Arc::new(Config::from_oauth(oauth));
337    /// let (ctx, mut receiver) = QuoteContext::new(config);
338    ///
339    /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
340    ///     .await?;
341    /// while let Some(msg) = receiver.recv().await {
342    ///     println!("{:?}", msg);
343    /// }
344    /// # Ok::<_, Box<dyn std::error::Error>>(())
345    /// # });
346    /// ```
347    pub async fn subscribe_candlesticks<T>(
348        &self,
349        symbol: T,
350        period: Period,
351        trade_sessions: TradeSessions,
352    ) -> Result<Vec<Candlestick>>
353    where
354        T: AsRef<str>,
355    {
356        let (reply_tx, reply_rx) = oneshot::channel();
357        self.0
358            .command_tx
359            .send(Command::SubscribeCandlesticks {
360                symbol: normalize_symbol(symbol.as_ref()).into(),
361                period,
362                trade_sessions,
363                reply_tx,
364            })
365            .map_err(|_| WsClientError::ClientClosed)?;
366        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
367    }
368
369    /// Unsubscribe security candlesticks
370    pub async fn unsubscribe_candlesticks<T>(&self, symbol: T, period: Period) -> Result<()>
371    where
372        T: AsRef<str>,
373    {
374        let (reply_tx, reply_rx) = oneshot::channel();
375        self.0
376            .command_tx
377            .send(Command::UnsubscribeCandlesticks {
378                symbol: normalize_symbol(symbol.as_ref()).into(),
379                period,
380                reply_tx,
381            })
382            .map_err(|_| WsClientError::ClientClosed)?;
383        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
384    }
385
386    /// Get subscription information
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// use std::sync::Arc;
392    ///
393    /// use longbridge::{
394    ///     Config,
395    ///     oauth::OAuthBuilder,
396    ///     quote::{QuoteContext, SubFlags},
397    /// };
398    ///
399    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
400    /// let oauth = OAuthBuilder::new("your-client-id")
401    ///     .build(|url| println!("Visit: {url}"))
402    ///     .await?;
403    /// let config = Arc::new(Config::from_oauth(oauth));
404    /// let (ctx, _) = QuoteContext::new(config);
405    ///
406    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
407    ///     .await?;
408    /// let resp = ctx.subscriptions().await?;
409    /// println!("{:?}", resp);
410    /// # Ok::<_, Box<dyn std::error::Error>>(())
411    /// # });
412    /// ```
413    pub async fn subscriptions(&self) -> Result<Vec<Subscription>> {
414        let (reply_tx, reply_rx) = oneshot::channel();
415        self.0
416            .command_tx
417            .send(Command::Subscriptions { reply_tx })
418            .map_err(|_| WsClientError::ClientClosed)?;
419        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
420    }
421
422    /// Get basic information of securities
423    ///
424    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/static>
425    ///
426    /// # Examples
427    ///
428    /// ```no_run
429    /// use std::sync::Arc;
430    ///
431    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
432    ///
433    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
434    /// let oauth = OAuthBuilder::new("your-client-id")
435    ///     .build(|url| println!("Visit: {url}"))
436    ///     .await?;
437    /// let config = Arc::new(Config::from_oauth(oauth));
438    /// let (ctx, _) = QuoteContext::new(config);
439    ///
440    /// let resp = ctx
441    ///     .static_info(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
442    ///     .await?;
443    /// println!("{:?}", resp);
444    /// # Ok::<_, Box<dyn std::error::Error>>(())
445    /// # });
446    /// ```
447    pub async fn static_info<I, T>(&self, symbols: I) -> Result<Vec<SecurityStaticInfo>>
448    where
449        I: IntoIterator<Item = T>,
450        T: Into<String>,
451    {
452        let resp: quote::SecurityStaticInfoResponse = self
453            .request(
454                cmd_code::GET_BASIC_INFO,
455                quote::MultiSecurityRequest {
456                    symbol: symbols.into_iter().map(Into::into).collect(),
457                },
458            )
459            .await?;
460        resp.secu_static_info
461            .into_iter()
462            .map(TryInto::try_into)
463            .collect()
464    }
465
466    /// Get quote of securities
467    ///
468    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/quote>
469    ///
470    /// # Examples
471    ///
472    /// ```no_run
473    /// use std::sync::Arc;
474    ///
475    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
476    ///
477    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
478    /// let oauth = OAuthBuilder::new("your-client-id")
479    ///     .build(|url| println!("Visit: {url}"))
480    ///     .await?;
481    /// let config = Arc::new(Config::from_oauth(oauth));
482    /// let (ctx, _) = QuoteContext::new(config);
483    ///
484    /// let resp = ctx
485    ///     .quote(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
486    ///     .await?;
487    /// println!("{:?}", resp);
488    /// # Ok::<_, Box<dyn std::error::Error>>(())
489    /// # });
490    /// ```
491    pub async fn quote<I, T>(&self, symbols: I) -> Result<Vec<SecurityQuote>>
492    where
493        I: IntoIterator<Item = T>,
494        T: Into<String>,
495    {
496        let resp: quote::SecurityQuoteResponse = self
497            .request(
498                cmd_code::GET_REALTIME_QUOTE,
499                quote::MultiSecurityRequest {
500                    symbol: symbols.into_iter().map(Into::into).collect(),
501                },
502            )
503            .await?;
504        resp.secu_quote.into_iter().map(TryInto::try_into).collect()
505    }
506
507    /// Get quote of option securities
508    ///
509    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/option-quote>
510    ///
511    /// # Examples
512    ///
513    /// ```no_run
514    /// use std::sync::Arc;
515    ///
516    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
517    ///
518    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
519    /// let oauth = OAuthBuilder::new("your-client-id")
520    ///     .build(|url| println!("Visit: {url}"))
521    ///     .await?;
522    /// let config = Arc::new(Config::from_oauth(oauth));
523    /// let (ctx, _) = QuoteContext::new(config);
524    ///
525    /// let resp = ctx.option_quote(["AAPL230317P160000.US"]).await?;
526    /// println!("{:?}", resp);
527    /// # Ok::<_, Box<dyn std::error::Error>>(())
528    /// # });
529    /// ```
530    pub async fn option_quote<I, T>(&self, symbols: I) -> Result<Vec<OptionQuote>>
531    where
532        I: IntoIterator<Item = T>,
533        T: Into<String>,
534    {
535        let resp: quote::OptionQuoteResponse = self
536            .request(
537                cmd_code::GET_REALTIME_OPTION_QUOTE,
538                quote::MultiSecurityRequest {
539                    symbol: symbols.into_iter().map(Into::into).collect(),
540                },
541            )
542            .await?;
543        resp.secu_quote.into_iter().map(TryInto::try_into).collect()
544    }
545
546    /// Get quote of warrant securities
547    ///
548    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/warrant-quote>
549    ///
550    /// # Examples
551    ///
552    /// ```no_run
553    /// use std::sync::Arc;
554    ///
555    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
556    ///
557    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
558    /// let oauth = OAuthBuilder::new("your-client-id")
559    ///     .build(|url| println!("Visit: {url}"))
560    ///     .await?;
561    /// let config = Arc::new(Config::from_oauth(oauth));
562    /// let (ctx, _) = QuoteContext::new(config);
563    ///
564    /// let resp = ctx.warrant_quote(["21125.HK"]).await?;
565    /// println!("{:?}", resp);
566    /// # Ok::<_, Box<dyn std::error::Error>>(())
567    /// # });
568    /// ```
569    pub async fn warrant_quote<I, T>(&self, symbols: I) -> Result<Vec<WarrantQuote>>
570    where
571        I: IntoIterator<Item = T>,
572        T: Into<String>,
573    {
574        let resp: quote::WarrantQuoteResponse = self
575            .request(
576                cmd_code::GET_REALTIME_WARRANT_QUOTE,
577                quote::MultiSecurityRequest {
578                    symbol: symbols.into_iter().map(Into::into).collect(),
579                },
580            )
581            .await?;
582        resp.secu_quote.into_iter().map(TryInto::try_into).collect()
583    }
584
585    /// Get security depth
586    ///
587    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/depth>
588    ///
589    /// # Examples
590    ///
591    /// ```no_run
592    /// use std::sync::Arc;
593    ///
594    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
595    ///
596    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
597    /// let oauth = OAuthBuilder::new("your-client-id")
598    ///     .build(|url| println!("Visit: {url}"))
599    ///     .await?;
600    /// let config = Arc::new(Config::from_oauth(oauth));
601    /// let (ctx, _) = QuoteContext::new(config);
602    ///
603    /// let resp = ctx.depth("700.HK").await?;
604    /// println!("{:?}", resp);
605    /// # Ok::<_, Box<dyn std::error::Error>>(())
606    /// # });
607    /// ```
608    pub async fn depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
609        let resp: quote::SecurityDepthResponse = self
610            .request(
611                cmd_code::GET_SECURITY_DEPTH,
612                quote::SecurityRequest {
613                    symbol: symbol.into(),
614                },
615            )
616            .await?;
617        Ok(SecurityDepth {
618            asks: resp
619                .ask
620                .into_iter()
621                .map(TryInto::try_into)
622                .collect::<Result<Vec<_>>>()?,
623            bids: resp
624                .bid
625                .into_iter()
626                .map(TryInto::try_into)
627                .collect::<Result<Vec<_>>>()?,
628        })
629    }
630
631    /// Get security brokers
632    ///
633    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/brokers>
634    ///
635    /// # Examples
636    ///
637    /// ```no_run
638    /// use std::sync::Arc;
639    ///
640    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
641    ///
642    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
643    /// let oauth = OAuthBuilder::new("your-client-id")
644    ///     .build(|url| println!("Visit: {url}"))
645    ///     .await?;
646    /// let config = Arc::new(Config::from_oauth(oauth));
647    /// let (ctx, _) = QuoteContext::new(config);
648    ///
649    /// let resp = ctx.brokers("700.HK").await?;
650    /// println!("{:?}", resp);
651    /// # Ok::<_, Box<dyn std::error::Error>>(())
652    /// # });
653    /// ```
654    pub async fn brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
655        let resp: quote::SecurityBrokersResponse = self
656            .request(
657                cmd_code::GET_SECURITY_BROKERS,
658                quote::SecurityRequest {
659                    symbol: symbol.into(),
660                },
661            )
662            .await?;
663        Ok(SecurityBrokers {
664            ask_brokers: resp.ask_brokers.into_iter().map(Into::into).collect(),
665            bid_brokers: resp.bid_brokers.into_iter().map(Into::into).collect(),
666        })
667    }
668
669    /// Get participants
670    ///
671    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/broker-ids>
672    ///
673    /// # Examples
674    ///
675    /// ```no_run
676    /// use std::sync::Arc;
677    ///
678    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
679    ///
680    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
681    /// let oauth = OAuthBuilder::new("your-client-id")
682    ///     .build(|url| println!("Visit: {url}"))
683    ///     .await?;
684    /// let config = Arc::new(Config::from_oauth(oauth));
685    /// let (ctx, _) = QuoteContext::new(config);
686    ///
687    /// let resp = ctx.participants().await?;
688    /// println!("{:?}", resp);
689    /// # Ok::<_, Box<dyn std::error::Error>>(())
690    /// # });
691    /// ```
692    pub async fn participants(&self) -> Result<Vec<ParticipantInfo>> {
693        self.0
694            .cache_participants
695            .get_or_update(|| async {
696                let resp = self
697                    .request_without_body::<quote::ParticipantBrokerIdsResponse>(
698                        cmd_code::GET_BROKER_IDS,
699                    )
700                    .await?;
701
702                Ok(resp
703                    .participant_broker_numbers
704                    .into_iter()
705                    .map(Into::into)
706                    .collect())
707            })
708            .await
709    }
710
711    /// Get security trades
712    ///
713    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade>
714    ///
715    /// # Examples
716    ///
717    /// ```no_run
718    /// use std::sync::Arc;
719    ///
720    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
721    ///
722    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
723    /// let oauth = OAuthBuilder::new("your-client-id")
724    ///     .build(|url| println!("Visit: {url}"))
725    ///     .await?;
726    /// let config = Arc::new(Config::from_oauth(oauth));
727    /// let (ctx, _) = QuoteContext::new(config);
728    ///
729    /// let resp = ctx.trades("700.HK", 10).await?;
730    /// println!("{:?}", resp);
731    /// # Ok::<_, Box<dyn std::error::Error>>(())
732    /// # });
733    /// ```
734    pub async fn trades(&self, symbol: impl Into<String>, count: usize) -> Result<Vec<Trade>> {
735        let resp: quote::SecurityTradeResponse = self
736            .request(
737                cmd_code::GET_SECURITY_TRADES,
738                quote::SecurityTradeRequest {
739                    symbol: symbol.into(),
740                    count: count as i32,
741                },
742            )
743            .await?;
744        let trades = resp
745            .trades
746            .into_iter()
747            .map(TryInto::try_into)
748            .collect::<Result<Vec<_>>>()?;
749        Ok(trades)
750    }
751
752    /// Get security intraday lines
753    ///
754    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/intraday>
755    ///
756    /// # Examples
757    ///
758    /// ```no_run
759    /// use std::sync::Arc;
760    ///
761    /// use longbridge::{
762    ///     Config,
763    ///     oauth::OAuthBuilder,
764    ///     quote::{QuoteContext, TradeSessions},
765    /// };
766    ///
767    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
768    /// let oauth = OAuthBuilder::new("your-client-id")
769    ///     .build(|url| println!("Visit: {url}"))
770    ///     .await?;
771    /// let config = Arc::new(Config::from_oauth(oauth));
772    /// let (ctx, _) = QuoteContext::new(config);
773    ///
774    /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
775    /// println!("{:?}", resp);
776    /// # Ok::<_, Box<dyn std::error::Error>>(())
777    /// # });
778    /// ```
779    pub async fn intraday(
780        &self,
781        symbol: impl Into<String>,
782        trade_sessions: TradeSessions,
783    ) -> Result<Vec<IntradayLine>> {
784        let resp: quote::SecurityIntradayResponse = self
785            .request(
786                cmd_code::GET_SECURITY_INTRADAY,
787                quote::SecurityIntradayRequest {
788                    symbol: symbol.into(),
789                    trade_session: trade_sessions as i32,
790                },
791            )
792            .await?;
793        let lines = resp
794            .lines
795            .into_iter()
796            .map(TryInto::try_into)
797            .collect::<Result<Vec<_>>>()?;
798        Ok(lines)
799    }
800
801    /// Get security candlesticks
802    ///
803    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/candlestick>
804    ///
805    /// # Examples
806    ///
807    /// ```no_run
808    /// use std::sync::Arc;
809    ///
810    /// use longbridge::{
811    ///     Config,
812    ///     oauth::OAuthBuilder,
813    ///     quote::{AdjustType, Period, QuoteContext, TradeSessions},
814    /// };
815    ///
816    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
817    /// let oauth = OAuthBuilder::new("your-client-id")
818    ///     .build(|url| println!("Visit: {url}"))
819    ///     .await?;
820    /// let config = Arc::new(Config::from_oauth(oauth));
821    /// let (ctx, _) = QuoteContext::new(config);
822    ///
823    /// let resp = ctx
824    ///     .candlesticks(
825    ///         "700.HK",
826    ///         Period::Day,
827    ///         10,
828    ///         AdjustType::NoAdjust,
829    ///         TradeSessions::Intraday,
830    ///     )
831    ///     .await?;
832    /// println!("{:?}", resp);
833    /// # Ok::<_, Box<dyn std::error::Error>>(())
834    /// # });
835    /// ```
836    pub async fn candlesticks(
837        &self,
838        symbol: impl Into<String>,
839        period: Period,
840        count: usize,
841        adjust_type: AdjustType,
842        trade_sessions: TradeSessions,
843    ) -> Result<Vec<Candlestick>> {
844        let resp: quote::SecurityCandlestickResponse = self
845            .request(
846                cmd_code::GET_SECURITY_CANDLESTICKS,
847                quote::SecurityCandlestickRequest {
848                    symbol: symbol.into(),
849                    period: period.into(),
850                    count: count as i32,
851                    adjust_type: adjust_type.into(),
852                    trade_session: trade_sessions as i32,
853                },
854            )
855            .await?;
856        let candlesticks = resp
857            .candlesticks
858            .into_iter()
859            .map(TryInto::try_into)
860            .collect::<Result<Vec<_>>>()?;
861        Ok(candlesticks)
862    }
863
864    /// Get security history candlesticks by offset
865    #[allow(clippy::too_many_arguments)]
866    pub async fn history_candlesticks_by_offset(
867        &self,
868        symbol: impl Into<String>,
869        period: Period,
870        adjust_type: AdjustType,
871        forward: bool,
872        time: Option<PrimitiveDateTime>,
873        count: usize,
874        trade_sessions: TradeSessions,
875    ) -> Result<Vec<Candlestick>> {
876        let resp: quote::SecurityCandlestickResponse = self
877            .request(
878                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
879                quote::SecurityHistoryCandlestickRequest {
880                    symbol: symbol.into(),
881                    period: period.into(),
882                    adjust_type: adjust_type.into(),
883                    query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
884                    offset_request: Some(
885                        quote::security_history_candlestick_request::OffsetQuery {
886                            direction: if forward {
887                                quote::Direction::Forward
888                            } else {
889                                quote::Direction::Backward
890                            }
891                            .into(),
892                            date: time
893                                .map(|time| {
894                                    format!(
895                                        "{:04}{:02}{:02}",
896                                        time.year(),
897                                        time.month() as u8,
898                                        time.day()
899                                    )
900                                })
901                                .unwrap_or_default(),
902                            minute: time
903                                .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
904                                .unwrap_or_default(),
905                            count: count as i32,
906                        },
907                    ),
908                    date_request: None,
909                    trade_session: trade_sessions as i32,
910                },
911            )
912            .await?;
913        let candlesticks = resp
914            .candlesticks
915            .into_iter()
916            .map(TryInto::try_into)
917            .collect::<Result<Vec<_>>>()?;
918        Ok(candlesticks)
919    }
920
921    /// Get security history candlesticks by date
922    pub async fn history_candlesticks_by_date(
923        &self,
924        symbol: impl Into<String>,
925        period: Period,
926        adjust_type: AdjustType,
927        start: Option<Date>,
928        end: Option<Date>,
929        trade_sessions: TradeSessions,
930    ) -> Result<Vec<Candlestick>> {
931        let resp: quote::SecurityCandlestickResponse = self
932            .request(
933                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
934                quote::SecurityHistoryCandlestickRequest {
935                    symbol: symbol.into(),
936                    period: period.into(),
937                    adjust_type: adjust_type.into(),
938                    query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
939                    offset_request: None,
940                    date_request: Some(quote::security_history_candlestick_request::DateQuery {
941                        start_date: start
942                            .map(|date| {
943                                format!(
944                                    "{:04}{:02}{:02}",
945                                    date.year(),
946                                    date.month() as u8,
947                                    date.day()
948                                )
949                            })
950                            .unwrap_or_default(),
951                        end_date: end
952                            .map(|date| {
953                                format!(
954                                    "{:04}{:02}{:02}",
955                                    date.year(),
956                                    date.month() as u8,
957                                    date.day()
958                                )
959                            })
960                            .unwrap_or_default(),
961                    }),
962                    trade_session: trade_sessions as i32,
963                },
964            )
965            .await?;
966        let candlesticks = resp
967            .candlesticks
968            .into_iter()
969            .map(TryInto::try_into)
970            .collect::<Result<Vec<_>>>()?;
971        Ok(candlesticks)
972    }
973
974    /// Get option chain expiry date list
975    ///
976    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date>
977    ///
978    /// # Examples
979    ///
980    /// ```no_run
981    /// use std::sync::Arc;
982    ///
983    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
984    ///
985    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
986    /// let oauth = OAuthBuilder::new("your-client-id")
987    ///     .build(|url| println!("Visit: {url}"))
988    ///     .await?;
989    /// let config = Arc::new(Config::from_oauth(oauth));
990    /// let (ctx, _) = QuoteContext::new(config);
991    ///
992    /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
993    /// println!("{:?}", resp);
994    /// # Ok::<_, Box<dyn std::error::Error>>(())
995    /// # });
996    /// ```
997    pub async fn option_chain_expiry_date_list(
998        &self,
999        symbol: impl Into<String>,
1000    ) -> Result<Vec<Date>> {
1001        self.0
1002            .cache_option_chain_expiry_date_list
1003            .get_or_update(symbol.into(), |symbol| async {
1004                let resp: quote::OptionChainDateListResponse = self
1005                    .request(
1006                        cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
1007                        quote::SecurityRequest { symbol },
1008                    )
1009                    .await?;
1010                resp.expiry_date
1011                    .iter()
1012                    .map(|value| {
1013                        parse_date(value).map_err(|err| Error::parse_field_error("date", err))
1014                    })
1015                    .collect::<Result<Vec<_>>>()
1016            })
1017            .await
1018    }
1019
1020    /// Get option chain info by date
1021    ///
1022    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date-strike>
1023    ///
1024    /// # Examples
1025    ///
1026    /// ```no_run
1027    /// use std::sync::Arc;
1028    ///
1029    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1030    /// use time::macros::date;
1031    ///
1032    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1033    /// let oauth = OAuthBuilder::new("your-client-id")
1034    ///     .build(|url| println!("Visit: {url}"))
1035    ///     .await?;
1036    /// let config = Arc::new(Config::from_oauth(oauth));
1037    /// let (ctx, _) = QuoteContext::new(config);
1038    ///
1039    /// let resp = ctx
1040    ///     .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))
1041    ///     .await?;
1042    /// println!("{:?}", resp);
1043    /// # Ok::<_, Box<dyn std::error::Error>>(())
1044    /// # });
1045    /// ```
1046    pub async fn option_chain_info_by_date(
1047        &self,
1048        symbol: impl Into<String>,
1049        expiry_date: Date,
1050    ) -> Result<Vec<StrikePriceInfo>> {
1051        self.0
1052            .cache_option_chain_strike_info
1053            .get_or_update(
1054                (symbol.into(), expiry_date),
1055                |(symbol, expiry_date)| async move {
1056                    let resp: quote::OptionChainDateStrikeInfoResponse = self
1057                        .request(
1058                            cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE,
1059                            quote::OptionChainDateStrikeInfoRequest {
1060                                symbol,
1061                                expiry_date: format_date(expiry_date),
1062                            },
1063                        )
1064                        .await?;
1065                    resp.strike_price_info
1066                        .into_iter()
1067                        .map(TryInto::try_into)
1068                        .collect::<Result<Vec<_>>>()
1069                },
1070            )
1071            .await
1072    }
1073
1074    /// Get warrant issuers
1075    ///
1076    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/issuer>
1077    ///
1078    /// # Examples
1079    ///
1080    /// ```no_run
1081    /// use std::sync::Arc;
1082    ///
1083    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1084    ///
1085    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1086    /// let oauth = OAuthBuilder::new("your-client-id")
1087    ///     .build(|url| println!("Visit: {url}"))
1088    ///     .await?;
1089    /// let config = Arc::new(Config::from_oauth(oauth));
1090    /// let (ctx, _) = QuoteContext::new(config);
1091    ///
1092    /// let resp = ctx.warrant_issuers().await?;
1093    /// println!("{:?}", resp);
1094    /// # Ok::<_, Box<dyn std::error::Error>>(())
1095    /// # });
1096    /// ```
1097    pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1098        self.0
1099            .cache_issuers
1100            .get_or_update(|| async {
1101                let resp = self
1102                    .request_without_body::<quote::IssuerInfoResponse>(
1103                        cmd_code::GET_WARRANT_ISSUER_IDS,
1104                    )
1105                    .await?;
1106                Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1107            })
1108            .await
1109    }
1110
1111    /// Query warrant list
1112    #[allow(clippy::too_many_arguments)]
1113    pub async fn warrant_list(
1114        &self,
1115        symbol: impl Into<String>,
1116        sort_by: WarrantSortBy,
1117        sort_order: SortOrderType,
1118        warrant_type: Option<&[WarrantType]>,
1119        issuer: Option<&[i32]>,
1120        expiry_date: Option<&[FilterWarrantExpiryDate]>,
1121        price_type: Option<&[FilterWarrantInOutBoundsType]>,
1122        status: Option<&[WarrantStatus]>,
1123    ) -> Result<Vec<WarrantInfo>> {
1124        let resp = self
1125            .request::<_, quote::WarrantFilterListResponse>(
1126                cmd_code::GET_FILTERED_WARRANT,
1127                quote::WarrantFilterListRequest {
1128                    symbol: symbol.into(),
1129                    filter_config: Some(quote::FilterConfig {
1130                        sort_by: sort_by.into(),
1131                        sort_order: sort_order.into(),
1132                        sort_offset: 0,
1133                        sort_count: 0,
1134                        r#type: warrant_type
1135                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1136                            .unwrap_or_default(),
1137                        issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1138                        expiry_date: expiry_date
1139                            .map(|e| e.iter().map(|e| (*e).into()).collect())
1140                            .unwrap_or_default(),
1141                        price_type: price_type
1142                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1143                            .unwrap_or_default(),
1144                        status: status
1145                            .map(|status| status.iter().map(|status| (*status).into()).collect())
1146                            .unwrap_or_default(),
1147                    }),
1148                    language: self.0.language.into(),
1149                },
1150            )
1151            .await?;
1152        resp.warrant_list
1153            .into_iter()
1154            .map(TryInto::try_into)
1155            .collect::<Result<Vec<_>>>()
1156    }
1157
1158    /// Get trading session of the day
1159    ///
1160    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-session>
1161    ///
1162    /// # Examples
1163    ///
1164    /// ```no_run
1165    /// use std::sync::Arc;
1166    ///
1167    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1168    ///
1169    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1170    /// let oauth = OAuthBuilder::new("your-client-id")
1171    ///     .build(|url| println!("Visit: {url}"))
1172    ///     .await?;
1173    /// let config = Arc::new(Config::from_oauth(oauth));
1174    /// let (ctx, _) = QuoteContext::new(config);
1175    ///
1176    /// let resp = ctx.trading_session().await?;
1177    /// println!("{:?}", resp);
1178    /// # Ok::<_, Box<dyn std::error::Error>>(())
1179    /// # });
1180    /// ```
1181    pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1182        self.0
1183            .cache_trading_session
1184            .get_or_update(|| async {
1185                let resp = self
1186                    .request_without_body::<quote::MarketTradePeriodResponse>(
1187                        cmd_code::GET_TRADING_SESSION,
1188                    )
1189                    .await?;
1190                resp.market_trade_session
1191                    .into_iter()
1192                    .map(TryInto::try_into)
1193                    .collect::<Result<Vec<_>>>()
1194            })
1195            .await
1196    }
1197
1198    /// Get market trading days
1199    ///
1200    /// The interval must be less than one month, and only the most recent year
1201    /// is supported.
1202    ///
1203    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-day>
1204    ///
1205    /// # Examples
1206    ///
1207    /// ```no_run
1208    /// use std::sync::Arc;
1209    ///
1210    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1211    /// use time::macros::date;
1212    ///
1213    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1214    /// let oauth = OAuthBuilder::new("your-client-id")
1215    ///     .build(|url| println!("Visit: {url}"))
1216    ///     .await?;
1217    /// let config = Arc::new(Config::from_oauth(oauth));
1218    /// let (ctx, _) = QuoteContext::new(config);
1219    ///
1220    /// let resp = ctx
1221    ///     .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1222    ///     .await?;
1223    /// println!("{:?}", resp);
1224    /// # Ok::<_, Box<dyn std::error::Error>>(())
1225    /// # });
1226    /// ```
1227    pub async fn trading_days(
1228        &self,
1229        market: Market,
1230        begin: Date,
1231        end: Date,
1232    ) -> Result<MarketTradingDays> {
1233        let resp = self
1234            .request::<_, quote::MarketTradeDayResponse>(
1235                cmd_code::GET_TRADING_DAYS,
1236                quote::MarketTradeDayRequest {
1237                    market: market.to_string(),
1238                    beg_day: format_date(begin),
1239                    end_day: format_date(end),
1240                },
1241            )
1242            .await?;
1243        let trading_days = resp
1244            .trade_day
1245            .iter()
1246            .map(|value| {
1247                parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1248            })
1249            .collect::<Result<Vec<_>>>()?;
1250        let half_trading_days = resp
1251            .half_trade_day
1252            .iter()
1253            .map(|value| {
1254                parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1255            })
1256            .collect::<Result<Vec<_>>>()?;
1257        Ok(MarketTradingDays {
1258            trading_days,
1259            half_trading_days,
1260        })
1261    }
1262
1263    /// Get capital flow intraday
1264    ///
1265    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-flow-intraday>
1266    ///
1267    /// # Examples
1268    ///
1269    /// ```no_run
1270    /// use std::sync::Arc;
1271    ///
1272    /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1273    ///
1274    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1275    /// let oauth = OAuthBuilder::new("your-client-id")
1276    ///     .build(|url| println!("Visit: {url}"))
1277    ///     .await?;
1278    /// let config = Arc::new(Config::from_oauth(oauth));
1279    /// let (ctx, _) = QuoteContext::new(config);
1280    ///
1281    /// let resp = ctx.capital_flow("700.HK").await?;
1282    /// println!("{:?}", resp);
1283    /// # Ok::<_, Box<dyn std::error::Error>>(())
1284    /// # });
1285    pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1286        self.request::<_, quote::CapitalFlowIntradayResponse>(
1287            cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1288            quote::CapitalFlowIntradayRequest {
1289                symbol: symbol.into(),
1290            },
1291        )
1292        .await?
1293        .capital_flow_lines
1294        .into_iter()
1295        .map(TryInto::try_into)
1296        .collect()
1297    }
1298
1299    /// Get capital distribution
1300    ///
1301    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-distribution>
1302    ///
1303    /// # Examples
1304    ///
1305    /// ```no_run
1306    /// use std::sync::Arc;
1307    ///
1308    /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1309    ///
1310    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1311    /// let oauth = OAuthBuilder::new("your-client-id")
1312    ///     .build(|url| println!("Visit: {url}"))
1313    ///     .await?;
1314    /// let config = Arc::new(Config::from_oauth(oauth));
1315    /// let (ctx, _) = QuoteContext::new(config);
1316    ///
1317    /// let resp = ctx.capital_distribution("700.HK").await?;
1318    /// println!("{:?}", resp);
1319    /// # Ok::<_, Box<dyn std::error::Error>>(())
1320    /// # });
1321    pub async fn capital_distribution(
1322        &self,
1323        symbol: impl Into<String>,
1324    ) -> Result<CapitalDistributionResponse> {
1325        self.request::<_, quote::CapitalDistributionResponse>(
1326            cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1327            quote::SecurityRequest {
1328                symbol: symbol.into(),
1329            },
1330        )
1331        .await?
1332        .try_into()
1333    }
1334
1335    /// Get calc indexes
1336    pub async fn calc_indexes<I, T, J>(
1337        &self,
1338        symbols: I,
1339        indexes: J,
1340    ) -> Result<Vec<SecurityCalcIndex>>
1341    where
1342        I: IntoIterator<Item = T>,
1343        T: Into<String>,
1344        J: IntoIterator<Item = CalcIndex>,
1345    {
1346        let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1347        let resp: quote::SecurityCalcQuoteResponse = self
1348            .request(
1349                cmd_code::GET_CALC_INDEXES,
1350                quote::SecurityCalcQuoteRequest {
1351                    symbols: symbols.into_iter().map(Into::into).collect(),
1352                    calc_index: indexes
1353                        .iter()
1354                        .map(|i| quote::CalcIndex::from(*i).into())
1355                        .collect(),
1356                },
1357            )
1358            .await?;
1359
1360        Ok(resp
1361            .security_calc_index
1362            .into_iter()
1363            .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1364            .collect())
1365    }
1366
1367    /// Get watchlist
1368    ///
1369    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_groups>
1370    ///
1371    /// # Examples
1372    ///
1373    /// ```no_run
1374    /// use std::sync::Arc;
1375    ///
1376    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1377    ///
1378    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1379    /// let oauth = OAuthBuilder::new("your-client-id")
1380    ///     .build(|url| println!("Visit: {url}"))
1381    ///     .await?;
1382    /// let config = Arc::new(Config::from_oauth(oauth));
1383    /// let (ctx, _) = QuoteContext::new(config);
1384    ///
1385    /// let resp = ctx.watchlist().await?;
1386    /// println!("{:?}", resp);
1387    /// # Ok::<_, Box<dyn std::error::Error>>(())
1388    /// # });
1389    /// ```
1390    pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1391        #[derive(Debug, Deserialize)]
1392        struct Response {
1393            groups: Vec<WatchlistGroup>,
1394        }
1395
1396        let resp = self
1397            .0
1398            .http_cli
1399            .request(Method::GET, "/v1/watchlist/groups")
1400            .response::<Json<Response>>()
1401            .send()
1402            .with_subscriber(self.0.log_subscriber.clone())
1403            .await?;
1404        Ok(resp.0.groups)
1405    }
1406
1407    /// Create watchlist group
1408    ///
1409    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_create_group>
1410    ///
1411    /// # Examples
1412    ///
1413    /// ```no_run
1414    /// use std::sync::Arc;
1415    ///
1416    /// use longbridge::{
1417    ///     Config,
1418    ///     oauth::OAuthBuilder,
1419    ///     quote::{QuoteContext, RequestCreateWatchlistGroup},
1420    /// };
1421    ///
1422    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1423    /// let oauth = OAuthBuilder::new("your-client-id")
1424    ///     .build(|url| println!("Visit: {url}"))
1425    ///     .await?;
1426    /// let config = Arc::new(Config::from_oauth(oauth));
1427    /// let (ctx, _) = QuoteContext::new(config);
1428    ///
1429    /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1430    /// let group_id = ctx.create_watchlist_group(req).await?;
1431    /// println!("{}", group_id);
1432    /// # Ok::<_, Box<dyn std::error::Error>>(())
1433    /// # });
1434    /// ```
1435    pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1436        #[derive(Debug, Serialize)]
1437        struct RequestCreate {
1438            name: String,
1439            #[serde(skip_serializing_if = "Option::is_none")]
1440            securities: Option<Vec<String>>,
1441        }
1442
1443        #[derive(Debug, Deserialize)]
1444        struct Response {
1445            #[serde(with = "serde_utils::int64_str")]
1446            id: i64,
1447        }
1448
1449        let Json(Response { id }) = self
1450            .0
1451            .http_cli
1452            .request(Method::POST, "/v1/watchlist/groups")
1453            .body(Json(RequestCreate {
1454                name: req.name,
1455                securities: req.securities,
1456            }))
1457            .response::<Json<Response>>()
1458            .send()
1459            .with_subscriber(self.0.log_subscriber.clone())
1460            .await?;
1461
1462        Ok(id)
1463    }
1464
1465    /// Delete watchlist group
1466    ///
1467    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_delete_group>
1468    ///
1469    /// # Examples
1470    ///
1471    /// ```no_run
1472    /// use std::sync::Arc;
1473    ///
1474    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1475    ///
1476    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1477    /// let oauth = OAuthBuilder::new("your-client-id")
1478    ///     .build(|url| println!("Visit: {url}"))
1479    ///     .await?;
1480    /// let config = Arc::new(Config::from_oauth(oauth));
1481    /// let (ctx, _) = QuoteContext::new(config);
1482    ///
1483    /// ctx.delete_watchlist_group(10086, true).await?;
1484    /// # Ok::<_, Box<dyn std::error::Error>>(())
1485    /// # });
1486    /// ```
1487    pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1488        #[derive(Debug, Serialize)]
1489        struct Request {
1490            id: i64,
1491            purge: bool,
1492        }
1493
1494        Ok(self
1495            .0
1496            .http_cli
1497            .request(Method::DELETE, "/v1/watchlist/groups")
1498            .query_params(Request { id, purge })
1499            .send()
1500            .with_subscriber(self.0.log_subscriber.clone())
1501            .await?)
1502    }
1503
1504    /// Update watchlist group
1505    ///
1506    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group>
1507    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group_securities>
1508    ///
1509    /// # Examples
1510    ///
1511    /// ```no_run
1512    /// use std::sync::Arc;
1513    ///
1514    /// use longbridge::{
1515    ///     Config,
1516    ///     oauth::OAuthBuilder,
1517    ///     quote::{QuoteContext, RequestUpdateWatchlistGroup},
1518    /// };
1519    ///
1520    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1521    /// let oauth = OAuthBuilder::new("your-client-id")
1522    ///     .build(|url| println!("Visit: {url}"))
1523    ///     .await?;
1524    /// let config = Arc::new(Config::from_oauth(oauth));
1525    /// let (ctx, _) = QuoteContext::new(config);
1526    /// let req = RequestUpdateWatchlistGroup::new(10086)
1527    ///     .name("Watchlist2")
1528    ///     .securities(["700.HK", "BABA.US"]);
1529    /// ctx.update_watchlist_group(req).await?;
1530    /// # Ok::<_, Box<dyn std::error::Error>>(())
1531    /// # });
1532    /// ```
1533    pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1534        #[derive(Debug, Serialize)]
1535        struct RequestUpdate {
1536            id: i64,
1537            #[serde(skip_serializing_if = "Option::is_none")]
1538            name: Option<String>,
1539            #[serde(skip_serializing_if = "Option::is_none")]
1540            securities: Option<Vec<String>>,
1541            #[serde(skip_serializing_if = "Option::is_none")]
1542            mode: Option<SecuritiesUpdateMode>,
1543        }
1544
1545        self.0
1546            .http_cli
1547            .request(Method::PUT, "/v1/watchlist/groups")
1548            .body(Json(RequestUpdate {
1549                id: req.id,
1550                name: req.name,
1551                mode: req.securities.is_some().then_some(req.mode),
1552                securities: req.securities,
1553            }))
1554            .send()
1555            .with_subscriber(self.0.log_subscriber.clone())
1556            .await?;
1557
1558        Ok(())
1559    }
1560
1561    /// Get security list
1562    pub async fn security_list(
1563        &self,
1564        market: Market,
1565        category: impl Into<Option<SecurityListCategory>>,
1566    ) -> Result<Vec<Security>> {
1567        #[derive(Debug, Serialize)]
1568        struct Request {
1569            market: Market,
1570            #[serde(skip_serializing_if = "Option::is_none")]
1571            category: Option<SecurityListCategory>,
1572        }
1573
1574        #[derive(Debug, Deserialize)]
1575        struct Response {
1576            list: Vec<Security>,
1577        }
1578
1579        Ok(self
1580            .0
1581            .http_cli
1582            .request(Method::GET, "/v1/quote/get_security_list")
1583            .query_params(Request {
1584                market,
1585                category: category.into(),
1586            })
1587            .response::<Json<Response>>()
1588            .send()
1589            .with_subscriber(self.0.log_subscriber.clone())
1590            .await?
1591            .0
1592            .list)
1593    }
1594
1595    /// Get filings list
1596    pub async fn filings(&self, symbol: impl Into<String>) -> Result<Vec<FilingItem>> {
1597        #[derive(Debug, Serialize)]
1598        struct Request {
1599            symbol: String,
1600        }
1601
1602        #[derive(Debug, Deserialize)]
1603        struct Response {
1604            items: Vec<FilingItem>,
1605        }
1606
1607        Ok(self
1608            .0
1609            .http_cli
1610            .request(Method::GET, "/v1/quote/filings")
1611            .query_params(Request {
1612                symbol: symbol.into(),
1613            })
1614            .response::<Json<Response>>()
1615            .send()
1616            .with_subscriber(self.0.log_subscriber.clone())
1617            .await?
1618            .0
1619            .items)
1620    }
1621
1622    /// Get current market temperature
1623    ///
1624    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/market_temperature>
1625    ///
1626    /// # Examples
1627    ///
1628    /// ```no_run
1629    /// use std::sync::Arc;
1630    ///
1631    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1632    ///
1633    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1634    /// let oauth = OAuthBuilder::new("your-client-id")
1635    ///     .build(|url| println!("Visit: {url}"))
1636    ///     .await?;
1637    /// let config = Arc::new(Config::from_oauth(oauth));
1638    /// let (ctx, _) = QuoteContext::new(config);
1639    ///
1640    /// let resp = ctx.market_temperature(Market::HK).await?;
1641    /// println!("{:?}", resp);
1642    /// # Ok::<_, Box<dyn std::error::Error>>(())
1643    /// # });
1644    /// ```
1645    pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1646        #[derive(Debug, Serialize)]
1647        struct Request {
1648            market: Market,
1649        }
1650
1651        Ok(self
1652            .0
1653            .http_cli
1654            .request(Method::GET, "/v1/quote/market_temperature")
1655            .query_params(Request { market })
1656            .response::<Json<MarketTemperature>>()
1657            .send()
1658            .with_subscriber(self.0.log_subscriber.clone())
1659            .await?
1660            .0)
1661    }
1662
1663    /// Get historical market temperature
1664    ///
1665    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/history_market_temperature>
1666    ///
1667    /// # Examples
1668    ///
1669    /// ```no_run
1670    /// use std::sync::Arc;
1671    ///
1672    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1673    /// use time::macros::date;
1674    ///
1675    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1676    /// let oauth = OAuthBuilder::new("your-client-id")
1677    ///     .build(|url| println!("Visit: {url}"))
1678    ///     .await?;
1679    /// let config = Arc::new(Config::from_oauth(oauth));
1680    /// let (ctx, _) = QuoteContext::new(config);
1681    ///
1682    /// let resp = ctx
1683    ///     .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1684    ///     .await?;
1685    /// println!("{:?}", resp);
1686    /// # Ok::<_, Box<dyn std::error::Error>>(())
1687    /// # });
1688    /// ```
1689    pub async fn history_market_temperature(
1690        &self,
1691        market: Market,
1692        start_date: Date,
1693        end_date: Date,
1694    ) -> Result<HistoryMarketTemperatureResponse> {
1695        #[derive(Debug, Serialize)]
1696        struct Request {
1697            market: Market,
1698            start_date: String,
1699            end_date: String,
1700        }
1701
1702        Ok(self
1703            .0
1704            .http_cli
1705            .request(Method::GET, "/v1/quote/history_market_temperature")
1706            .query_params(Request {
1707                market,
1708                start_date: format_date(start_date),
1709                end_date: format_date(end_date),
1710            })
1711            .response::<Json<HistoryMarketTemperatureResponse>>()
1712            .send()
1713            .with_subscriber(self.0.log_subscriber.clone())
1714            .await?
1715            .0)
1716    }
1717
1718    /// Get real-time quotes
1719    ///
1720    /// Get real-time quotes of the subscribed symbols, it always returns the
1721    /// data in the local storage.
1722    ///
1723    /// # Examples
1724    ///
1725    /// ```no_run
1726    /// use std::{sync::Arc, time::Duration};
1727    ///
1728    /// use longbridge::{
1729    ///     Config,
1730    ///     oauth::OAuthBuilder,
1731    ///     quote::{QuoteContext, SubFlags},
1732    /// };
1733    ///
1734    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1735    /// let oauth = OAuthBuilder::new("your-client-id")
1736    ///     .build(|url| println!("Visit: {url}"))
1737    ///     .await?;
1738    /// let config = Arc::new(Config::from_oauth(oauth));
1739    /// let (ctx, _) = QuoteContext::new(config);
1740    ///
1741    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
1742    ///     .await?;
1743    /// tokio::time::sleep(Duration::from_secs(5)).await;
1744    ///
1745    /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
1746    /// println!("{:?}", resp);
1747    /// # Ok::<_, Box<dyn std::error::Error>>(())
1748    /// # });
1749    /// ```
1750    pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
1751    where
1752        I: IntoIterator<Item = T>,
1753        T: Into<String>,
1754    {
1755        let (reply_tx, reply_rx) = oneshot::channel();
1756        self.0
1757            .command_tx
1758            .send(Command::GetRealtimeQuote {
1759                symbols: symbols.into_iter().map(Into::into).collect(),
1760                reply_tx,
1761            })
1762            .map_err(|_| WsClientError::ClientClosed)?;
1763        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1764    }
1765
1766    /// Get real-time depth
1767    ///
1768    /// Get real-time depth of the subscribed symbols, it always returns the
1769    /// data in the local storage.
1770    ///
1771    /// # Examples
1772    ///
1773    /// ```no_run
1774    /// use std::{sync::Arc, time::Duration};
1775    ///
1776    /// use longbridge::{
1777    ///     Config,
1778    ///     oauth::OAuthBuilder,
1779    ///     quote::{QuoteContext, SubFlags},
1780    /// };
1781    ///
1782    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1783    /// let oauth = OAuthBuilder::new("your-client-id")
1784    ///     .build(|url| println!("Visit: {url}"))
1785    ///     .await?;
1786    /// let config = Arc::new(Config::from_oauth(oauth));
1787    /// let (ctx, _) = QuoteContext::new(config);
1788    ///
1789    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH)
1790    ///     .await?;
1791    /// tokio::time::sleep(Duration::from_secs(5)).await;
1792    ///
1793    /// let resp = ctx.realtime_depth("700.HK").await?;
1794    /// println!("{:?}", resp);
1795    /// # Ok::<_, Box<dyn std::error::Error>>(())
1796    /// # });
1797    /// ```
1798    pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
1799        let (reply_tx, reply_rx) = oneshot::channel();
1800        self.0
1801            .command_tx
1802            .send(Command::GetRealtimeDepth {
1803                symbol: symbol.into(),
1804                reply_tx,
1805            })
1806            .map_err(|_| WsClientError::ClientClosed)?;
1807        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1808    }
1809
1810    /// Get real-time trades
1811    ///
1812    /// Get real-time trades of the subscribed symbols, it always returns the
1813    /// data in the local storage.
1814    ///
1815    /// # Examples
1816    ///
1817    /// ```no_run
1818    /// use std::{sync::Arc, time::Duration};
1819    ///
1820    /// use longbridge::{
1821    ///     Config,
1822    ///     oauth::OAuthBuilder,
1823    ///     quote::{QuoteContext, SubFlags},
1824    /// };
1825    ///
1826    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1827    /// let oauth = OAuthBuilder::new("your-client-id")
1828    ///     .build(|url| println!("Visit: {url}"))
1829    ///     .await?;
1830    /// let config = Arc::new(Config::from_oauth(oauth));
1831    /// let (ctx, _) = QuoteContext::new(config);
1832    ///
1833    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE)
1834    ///     .await?;
1835    /// tokio::time::sleep(Duration::from_secs(5)).await;
1836    ///
1837    /// let resp = ctx.realtime_trades("700.HK", 10).await?;
1838    /// println!("{:?}", resp);
1839    /// # Ok::<_, Box<dyn std::error::Error>>(())
1840    /// # });
1841    /// ```
1842    pub async fn realtime_trades(
1843        &self,
1844        symbol: impl Into<String>,
1845        count: usize,
1846    ) -> Result<Vec<Trade>> {
1847        let (reply_tx, reply_rx) = oneshot::channel();
1848        self.0
1849            .command_tx
1850            .send(Command::GetRealtimeTrade {
1851                symbol: symbol.into(),
1852                count,
1853                reply_tx,
1854            })
1855            .map_err(|_| WsClientError::ClientClosed)?;
1856        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1857    }
1858
1859    /// Get real-time broker queue
1860    ///
1861    ///
1862    /// Get real-time broker queue of the subscribed symbols, it always returns
1863    /// the data in the local storage.
1864    ///
1865    /// # Examples
1866    ///
1867    /// ```no_run
1868    /// use std::{sync::Arc, time::Duration};
1869    ///
1870    /// use longbridge::{
1871    ///     Config,
1872    ///     oauth::OAuthBuilder,
1873    ///     quote::{QuoteContext, SubFlags},
1874    /// };
1875    ///
1876    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1877    /// let oauth = OAuthBuilder::new("your-client-id")
1878    ///     .build(|url| println!("Visit: {url}"))
1879    ///     .await?;
1880    /// let config = Arc::new(Config::from_oauth(oauth));
1881    /// let (ctx, _) = QuoteContext::new(config);
1882    ///
1883    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER)
1884    ///     .await?;
1885    /// tokio::time::sleep(Duration::from_secs(5)).await;
1886    ///
1887    /// let resp = ctx.realtime_brokers("700.HK").await?;
1888    /// println!("{:?}", resp);
1889    /// # Ok::<_, Box<dyn std::error::Error>>(())
1890    /// # });
1891    /// ```
1892    pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
1893        let (reply_tx, reply_rx) = oneshot::channel();
1894        self.0
1895            .command_tx
1896            .send(Command::GetRealtimeBrokers {
1897                symbol: symbol.into(),
1898                reply_tx,
1899            })
1900            .map_err(|_| WsClientError::ClientClosed)?;
1901        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1902    }
1903
1904    /// Get real-time candlesticks
1905    ///
1906    /// Get real-time candlesticks of the subscribed symbols, it always returns
1907    /// the data in the local storage.
1908    ///
1909    /// # Examples
1910    ///
1911    /// ```no_run
1912    /// use std::{sync::Arc, time::Duration};
1913    ///
1914    /// use longbridge::{
1915    ///     Config,
1916    ///     oauth::OAuthBuilder,
1917    ///     quote::{Period, QuoteContext, TradeSessions},
1918    /// };
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    /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
1928    ///     .await?;
1929    /// tokio::time::sleep(Duration::from_secs(5)).await;
1930    ///
1931    /// let resp = ctx
1932    ///     .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
1933    ///     .await?;
1934    /// println!("{:?}", resp);
1935    /// # Ok::<_, Box<dyn std::error::Error>>(())
1936    /// # });
1937    /// ```
1938    pub async fn realtime_candlesticks(
1939        &self,
1940        symbol: impl Into<String>,
1941        period: Period,
1942        count: usize,
1943    ) -> Result<Vec<Candlestick>> {
1944        let (reply_tx, reply_rx) = oneshot::channel();
1945        self.0
1946            .command_tx
1947            .send(Command::GetRealtimeCandlesticks {
1948                symbol: symbol.into(),
1949                period,
1950                count,
1951                reply_tx,
1952            })
1953            .map_err(|_| WsClientError::ClientClosed)?;
1954        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1955    }
1956}
1957
1958fn normalize_symbol(symbol: &str) -> &str {
1959    match symbol.split_once('.') {
1960        Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
1961        _ => symbol,
1962    }
1963}