longbridge/quote/
context.rs

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