longport/quote/
context.rs

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