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::{
698    ///     Config,
699    ///     quote::{QuoteContext, TradeSessions},
700    /// };
701    ///
702    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
703    /// let config = Arc::new(Config::from_env()?);
704    /// let (ctx, _) = QuoteContext::try_new(config).await?;
705    ///
706    /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
707    /// println!("{:?}", resp);
708    /// # Ok::<_, Box<dyn std::error::Error>>(())
709    /// # });
710    /// ```
711    pub async fn intraday(
712        &self,
713        symbol: impl Into<String>,
714        trade_sessions: TradeSessions,
715    ) -> Result<Vec<IntradayLine>> {
716        let resp: quote::SecurityIntradayResponse = self
717            .request(
718                cmd_code::GET_SECURITY_INTRADAY,
719                quote::SecurityIntradayRequest {
720                    symbol: symbol.into(),
721                    trade_session: trade_sessions as i32,
722                },
723            )
724            .await?;
725        let lines = resp
726            .lines
727            .into_iter()
728            .map(TryInto::try_into)
729            .collect::<Result<Vec<_>>>()?;
730        Ok(lines)
731    }
732
733    /// Get security candlesticks
734    ///
735    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/candlestick>
736    ///
737    /// # Examples
738    ///
739    /// ```no_run
740    /// use std::sync::Arc;
741    ///
742    /// use longport::{
743    ///     Config,
744    ///     quote::{AdjustType, Period, QuoteContext, TradeSessions},
745    /// };
746    ///
747    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
748    /// let config = Arc::new(Config::from_env()?);
749    /// let (ctx, _) = QuoteContext::try_new(config).await?;
750    ///
751    /// let resp = ctx
752    ///     .candlesticks(
753    ///         "700.HK",
754    ///         Period::Day,
755    ///         10,
756    ///         AdjustType::NoAdjust,
757    ///         TradeSessions::Intraday,
758    ///     )
759    ///     .await?;
760    /// println!("{:?}", resp);
761    /// # Ok::<_, Box<dyn std::error::Error>>(())
762    /// # });
763    /// ```
764    pub async fn candlesticks(
765        &self,
766        symbol: impl Into<String>,
767        period: Period,
768        count: usize,
769        adjust_type: AdjustType,
770        trade_sessions: TradeSessions,
771    ) -> Result<Vec<Candlestick>> {
772        let resp: quote::SecurityCandlestickResponse = self
773            .request(
774                cmd_code::GET_SECURITY_CANDLESTICKS,
775                quote::SecurityCandlestickRequest {
776                    symbol: symbol.into(),
777                    period: period.into(),
778                    count: count as i32,
779                    adjust_type: adjust_type.into(),
780                    trade_session: trade_sessions as i32,
781                },
782            )
783            .await?;
784        let candlesticks = resp
785            .candlesticks
786            .into_iter()
787            .map(TryInto::try_into)
788            .collect::<Result<Vec<_>>>()?;
789        Ok(candlesticks)
790    }
791
792    /// Get security history candlesticks by offset
793    #[allow(clippy::too_many_arguments)]
794    pub async fn history_candlesticks_by_offset(
795        &self,
796        symbol: impl Into<String>,
797        period: Period,
798        adjust_type: AdjustType,
799        forward: bool,
800        time: Option<PrimitiveDateTime>,
801        count: usize,
802        trade_sessions: TradeSessions,
803    ) -> Result<Vec<Candlestick>> {
804        let resp: quote::SecurityCandlestickResponse = self
805            .request(
806                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
807                quote::SecurityHistoryCandlestickRequest {
808                    symbol: symbol.into(),
809                    period: period.into(),
810                    adjust_type: adjust_type.into(),
811                    query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
812                    offset_request: Some(
813                        quote::security_history_candlestick_request::OffsetQuery {
814                            direction: if forward {
815                                quote::Direction::Forward
816                            } else {
817                                quote::Direction::Backward
818                            }
819                            .into(),
820                            date: time
821                                .map(|time| {
822                                    format!(
823                                        "{:04}{:02}{:02}",
824                                        time.year(),
825                                        time.month() as u8,
826                                        time.day()
827                                    )
828                                })
829                                .unwrap_or_default(),
830                            minute: time
831                                .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
832                                .unwrap_or_default(),
833                            count: count as i32,
834                        },
835                    ),
836                    date_request: None,
837                    trade_session: trade_sessions as i32,
838                },
839            )
840            .await?;
841        let candlesticks = resp
842            .candlesticks
843            .into_iter()
844            .map(TryInto::try_into)
845            .collect::<Result<Vec<_>>>()?;
846        Ok(candlesticks)
847    }
848
849    /// Get security history candlesticks by date
850    pub async fn history_candlesticks_by_date(
851        &self,
852        symbol: impl Into<String>,
853        period: Period,
854        adjust_type: AdjustType,
855        start: Option<Date>,
856        end: Option<Date>,
857        trade_sessions: TradeSessions,
858    ) -> Result<Vec<Candlestick>> {
859        let resp: quote::SecurityCandlestickResponse = self
860            .request(
861                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
862                quote::SecurityHistoryCandlestickRequest {
863                    symbol: symbol.into(),
864                    period: period.into(),
865                    adjust_type: adjust_type.into(),
866                    query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
867                    offset_request: None,
868                    date_request: Some(quote::security_history_candlestick_request::DateQuery {
869                        start_date: start
870                            .map(|date| {
871                                format!(
872                                    "{:04}{:02}{:02}",
873                                    date.year(),
874                                    date.month() as u8,
875                                    date.day()
876                                )
877                            })
878                            .unwrap_or_default(),
879                        end_date: end
880                            .map(|date| {
881                                format!(
882                                    "{:04}{:02}{:02}",
883                                    date.year(),
884                                    date.month() as u8,
885                                    date.day()
886                                )
887                            })
888                            .unwrap_or_default(),
889                    }),
890                    trade_session: trade_sessions as i32,
891                },
892            )
893            .await?;
894        let candlesticks = resp
895            .candlesticks
896            .into_iter()
897            .map(TryInto::try_into)
898            .collect::<Result<Vec<_>>>()?;
899        Ok(candlesticks)
900    }
901
902    /// Get option chain expiry date list
903    ///
904    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/optionchain-date>
905    ///
906    /// # Examples
907    ///
908    /// ```no_run
909    /// use std::sync::Arc;
910    ///
911    /// use longport::{Config, quote::QuoteContext};
912    ///
913    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
914    /// let config = Arc::new(Config::from_env()?);
915    /// let (ctx, _) = QuoteContext::try_new(config).await?;
916    ///
917    /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
918    /// println!("{:?}", resp);
919    /// # Ok::<_, Box<dyn std::error::Error>>(())
920    /// # });
921    /// ```
922    pub async fn option_chain_expiry_date_list(
923        &self,
924        symbol: impl Into<String>,
925    ) -> Result<Vec<Date>> {
926        self.0
927            .cache_option_chain_expiry_date_list
928            .get_or_update(symbol.into(), |symbol| async {
929                let resp: quote::OptionChainDateListResponse = self
930                    .request(
931                        cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
932                        quote::SecurityRequest { symbol },
933                    )
934                    .await?;
935                resp.expiry_date
936                    .iter()
937                    .map(|value| {
938                        parse_date(value).map_err(|err| Error::parse_field_error("date", err))
939                    })
940                    .collect::<Result<Vec<_>>>()
941            })
942            .await
943    }
944
945    /// Get option chain info by date
946    ///
947    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/optionchain-date-strike>
948    ///
949    /// # Examples
950    ///
951    /// ```no_run
952    /// use std::sync::Arc;
953    ///
954    /// use longport::{Config, quote::QuoteContext};
955    /// use time::macros::date;
956    ///
957    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
958    /// let config = Arc::new(Config::from_env()?);
959    /// let (ctx, _) = QuoteContext::try_new(config).await?;
960    ///
961    /// let resp = ctx
962    ///     .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))
963    ///     .await?;
964    /// println!("{:?}", resp);
965    /// # Ok::<_, Box<dyn std::error::Error>>(())
966    /// # });
967    /// ```
968    pub async fn option_chain_info_by_date(
969        &self,
970        symbol: impl Into<String>,
971        expiry_date: Date,
972    ) -> Result<Vec<StrikePriceInfo>> {
973        self.0
974            .cache_option_chain_strike_info
975            .get_or_update(
976                (symbol.into(), expiry_date),
977                |(symbol, expiry_date)| async move {
978                    let resp: quote::OptionChainDateStrikeInfoResponse = self
979                        .request(
980                            cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE,
981                            quote::OptionChainDateStrikeInfoRequest {
982                                symbol,
983                                expiry_date: format_date(expiry_date),
984                            },
985                        )
986                        .await?;
987                    resp.strike_price_info
988                        .into_iter()
989                        .map(TryInto::try_into)
990                        .collect::<Result<Vec<_>>>()
991                },
992            )
993            .await
994    }
995
996    /// Get warrant issuers
997    ///
998    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/issuer>
999    ///
1000    /// # Examples
1001    ///
1002    /// ```no_run
1003    /// use std::sync::Arc;
1004    ///
1005    /// use longport::{Config, quote::QuoteContext};
1006    ///
1007    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1008    /// let config = Arc::new(Config::from_env()?);
1009    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1010    ///
1011    /// let resp = ctx.warrant_issuers().await?;
1012    /// println!("{:?}", resp);
1013    /// # Ok::<_, Box<dyn std::error::Error>>(())
1014    /// # });
1015    /// ```
1016    pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1017        self.0
1018            .cache_issuers
1019            .get_or_update(|| async {
1020                let resp = self
1021                    .request_without_body::<quote::IssuerInfoResponse>(
1022                        cmd_code::GET_WARRANT_ISSUER_IDS,
1023                    )
1024                    .await?;
1025                Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1026            })
1027            .await
1028    }
1029
1030    /// Query warrant list
1031    #[allow(clippy::too_many_arguments)]
1032    pub async fn warrant_list(
1033        &self,
1034        symbol: impl Into<String>,
1035        sort_by: WarrantSortBy,
1036        sort_order: SortOrderType,
1037        warrant_type: Option<&[WarrantType]>,
1038        issuer: Option<&[i32]>,
1039        expiry_date: Option<&[FilterWarrantExpiryDate]>,
1040        price_type: Option<&[FilterWarrantInOutBoundsType]>,
1041        status: Option<&[WarrantStatus]>,
1042    ) -> Result<Vec<WarrantInfo>> {
1043        let resp = self
1044            .request::<_, quote::WarrantFilterListResponse>(
1045                cmd_code::GET_FILTERED_WARRANT,
1046                quote::WarrantFilterListRequest {
1047                    symbol: symbol.into(),
1048                    filter_config: Some(quote::FilterConfig {
1049                        sort_by: sort_by.into(),
1050                        sort_order: sort_order.into(),
1051                        sort_offset: 0,
1052                        sort_count: 0,
1053                        r#type: warrant_type
1054                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1055                            .unwrap_or_default(),
1056                        issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1057                        expiry_date: expiry_date
1058                            .map(|e| e.iter().map(|e| (*e).into()).collect())
1059                            .unwrap_or_default(),
1060                        price_type: price_type
1061                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1062                            .unwrap_or_default(),
1063                        status: status
1064                            .map(|status| status.iter().map(|status| (*status).into()).collect())
1065                            .unwrap_or_default(),
1066                    }),
1067                    language: self.0.language.into(),
1068                },
1069            )
1070            .await?;
1071        resp.warrant_list
1072            .into_iter()
1073            .map(TryInto::try_into)
1074            .collect::<Result<Vec<_>>>()
1075    }
1076
1077    /// Get trading session of the day
1078    ///
1079    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/trade-session>
1080    ///
1081    /// # Examples
1082    ///
1083    /// ```no_run
1084    /// use std::sync::Arc;
1085    ///
1086    /// use longport::{Config, quote::QuoteContext};
1087    ///
1088    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1089    /// let config = Arc::new(Config::from_env()?);
1090    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1091    ///
1092    /// let resp = ctx.trading_session().await?;
1093    /// println!("{:?}", resp);
1094    /// # Ok::<_, Box<dyn std::error::Error>>(())
1095    /// # });
1096    /// ```
1097    pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1098        self.0
1099            .cache_trading_session
1100            .get_or_update(|| async {
1101                let resp = self
1102                    .request_without_body::<quote::MarketTradePeriodResponse>(
1103                        cmd_code::GET_TRADING_SESSION,
1104                    )
1105                    .await?;
1106                resp.market_trade_session
1107                    .into_iter()
1108                    .map(TryInto::try_into)
1109                    .collect::<Result<Vec<_>>>()
1110            })
1111            .await
1112    }
1113
1114    /// Get market trading days
1115    ///
1116    /// The interval must be less than one month, and only the most recent year
1117    /// is supported.
1118    ///
1119    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/trade-day>
1120    ///
1121    /// # Examples
1122    ///
1123    /// ```no_run
1124    /// use std::sync::Arc;
1125    ///
1126    /// use longport::{Config, Market, quote::QuoteContext};
1127    /// use time::macros::date;
1128    ///
1129    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1130    /// let config = Arc::new(Config::from_env()?);
1131    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1132    ///
1133    /// let resp = ctx
1134    ///     .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1135    ///     .await?;
1136    /// println!("{:?}", resp);
1137    /// # Ok::<_, Box<dyn std::error::Error>>(())
1138    /// # });
1139    /// ```
1140    pub async fn trading_days(
1141        &self,
1142        market: Market,
1143        begin: Date,
1144        end: Date,
1145    ) -> Result<MarketTradingDays> {
1146        let resp = self
1147            .request::<_, quote::MarketTradeDayResponse>(
1148                cmd_code::GET_TRADING_DAYS,
1149                quote::MarketTradeDayRequest {
1150                    market: market.to_string(),
1151                    beg_day: format_date(begin),
1152                    end_day: format_date(end),
1153                },
1154            )
1155            .await?;
1156        let trading_days = resp
1157            .trade_day
1158            .iter()
1159            .map(|value| {
1160                parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1161            })
1162            .collect::<Result<Vec<_>>>()?;
1163        let half_trading_days = resp
1164            .half_trade_day
1165            .iter()
1166            .map(|value| {
1167                parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1168            })
1169            .collect::<Result<Vec<_>>>()?;
1170        Ok(MarketTradingDays {
1171            trading_days,
1172            half_trading_days,
1173        })
1174    }
1175
1176    /// Get capital flow intraday
1177    ///
1178    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/capital-flow-intraday>
1179    ///
1180    /// # Examples
1181    ///
1182    /// ```no_run
1183    /// use std::sync::Arc;
1184    ///
1185    /// use longport::{quote::QuoteContext, Config};
1186    ///
1187    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1188    /// let config = Arc::new(Config::from_env()?);
1189    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1190    ///
1191    /// let resp = ctx.capital_flow("700.HK").await?;
1192    /// println!("{:?}", resp);
1193    /// # Ok::<_, Box<dyn std::error::Error>>(())
1194    /// # });
1195    pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1196        self.request::<_, quote::CapitalFlowIntradayResponse>(
1197            cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1198            quote::CapitalFlowIntradayRequest {
1199                symbol: symbol.into(),
1200            },
1201        )
1202        .await?
1203        .capital_flow_lines
1204        .into_iter()
1205        .map(TryInto::try_into)
1206        .collect()
1207    }
1208
1209    /// Get capital distribution
1210    ///
1211    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/capital-distribution>
1212    ///
1213    /// # Examples
1214    ///
1215    /// ```no_run
1216    /// use std::sync::Arc;
1217    ///
1218    /// use longport::{quote::QuoteContext, Config};
1219    ///
1220    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1221    /// let config = Arc::new(Config::from_env()?);
1222    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1223    ///
1224    /// let resp = ctx.capital_distribution("700.HK").await?;
1225    /// println!("{:?}", resp);
1226    /// # Ok::<_, Box<dyn std::error::Error>>(())
1227    /// # });
1228    pub async fn capital_distribution(
1229        &self,
1230        symbol: impl Into<String>,
1231    ) -> Result<CapitalDistributionResponse> {
1232        self.request::<_, quote::CapitalDistributionResponse>(
1233            cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1234            quote::SecurityRequest {
1235                symbol: symbol.into(),
1236            },
1237        )
1238        .await?
1239        .try_into()
1240    }
1241
1242    /// Get calc indexes
1243    pub async fn calc_indexes<I, T, J>(
1244        &self,
1245        symbols: I,
1246        indexes: J,
1247    ) -> Result<Vec<SecurityCalcIndex>>
1248    where
1249        I: IntoIterator<Item = T>,
1250        T: Into<String>,
1251        J: IntoIterator<Item = CalcIndex>,
1252    {
1253        let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1254        let resp: quote::SecurityCalcQuoteResponse = self
1255            .request(
1256                cmd_code::GET_CALC_INDEXES,
1257                quote::SecurityCalcQuoteRequest {
1258                    symbols: symbols.into_iter().map(Into::into).collect(),
1259                    calc_index: indexes
1260                        .iter()
1261                        .map(|i| quote::CalcIndex::from(*i).into())
1262                        .collect(),
1263                },
1264            )
1265            .await?;
1266
1267        Ok(resp
1268            .security_calc_index
1269            .into_iter()
1270            .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1271            .collect())
1272    }
1273
1274    /// Get watchlist
1275    ///
1276    /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_groups>
1277    ///
1278    /// # Examples
1279    ///
1280    /// ```no_run
1281    /// use std::sync::Arc;
1282    ///
1283    /// use longport::{Config, quote::QuoteContext};
1284    ///
1285    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1286    /// let config = Arc::new(Config::from_env()?);
1287    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1288    ///
1289    /// let resp = ctx.watchlist().await?;
1290    /// println!("{:?}", resp);
1291    /// # Ok::<_, Box<dyn std::error::Error>>(())
1292    /// # });
1293    /// ```
1294    pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1295        #[derive(Debug, Deserialize)]
1296        struct Response {
1297            groups: Vec<WatchlistGroup>,
1298        }
1299
1300        let resp = self
1301            .0
1302            .http_cli
1303            .request(Method::GET, "/v1/watchlist/groups")
1304            .response::<Json<Response>>()
1305            .send()
1306            .with_subscriber(self.0.log_subscriber.clone())
1307            .await?;
1308        Ok(resp.0.groups)
1309    }
1310
1311    /// Create watchlist group
1312    ///
1313    /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_create_group>
1314    ///
1315    /// # Examples
1316    ///
1317    /// ```no_run
1318    /// use std::sync::Arc;
1319    ///
1320    /// use longport::{
1321    ///     Config,
1322    ///     quote::{QuoteContext, RequestCreateWatchlistGroup},
1323    /// };
1324    ///
1325    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1326    /// let config = Arc::new(Config::from_env()?);
1327    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1328    ///
1329    /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1330    /// let group_id = ctx.create_watchlist_group(req).await?;
1331    /// println!("{}", group_id);
1332    /// # Ok::<_, Box<dyn std::error::Error>>(())
1333    /// # });
1334    /// ```
1335    pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1336        #[derive(Debug, Serialize)]
1337        struct RequestCreate {
1338            name: String,
1339            #[serde(skip_serializing_if = "Option::is_none")]
1340            securities: Option<Vec<String>>,
1341        }
1342
1343        #[derive(Debug, Deserialize)]
1344        struct Response {
1345            #[serde(with = "serde_utils::int64_str")]
1346            id: i64,
1347        }
1348
1349        let Json(Response { id }) = self
1350            .0
1351            .http_cli
1352            .request(Method::POST, "/v1/watchlist/groups")
1353            .body(Json(RequestCreate {
1354                name: req.name,
1355                securities: req.securities,
1356            }))
1357            .response::<Json<Response>>()
1358            .send()
1359            .with_subscriber(self.0.log_subscriber.clone())
1360            .await?;
1361
1362        Ok(id)
1363    }
1364
1365    /// Delete watchlist group
1366    ///
1367    /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_delete_group>
1368    ///
1369    /// # Examples
1370    ///
1371    /// ```no_run
1372    /// use std::sync::Arc;
1373    ///
1374    /// use longport::{Config, quote::QuoteContext};
1375    ///
1376    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1377    /// let config = Arc::new(Config::from_env()?);
1378    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1379    ///
1380    /// ctx.delete_watchlist_group(10086, true).await?;
1381    /// # Ok::<_, Box<dyn std::error::Error>>(())
1382    /// # });
1383    /// ```
1384    pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1385        #[derive(Debug, Serialize)]
1386        struct Request {
1387            id: i64,
1388            purge: bool,
1389        }
1390
1391        Ok(self
1392            .0
1393            .http_cli
1394            .request(Method::DELETE, "/v1/watchlist/groups")
1395            .query_params(Request { id, purge })
1396            .send()
1397            .with_subscriber(self.0.log_subscriber.clone())
1398            .await?)
1399    }
1400
1401    /// Update watchlist group
1402    ///
1403    /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_update_group>
1404    /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_update_group_securities>
1405    ///
1406    /// # Examples
1407    ///
1408    /// ```no_run
1409    /// use std::sync::Arc;
1410    ///
1411    /// use longport::{
1412    ///     Config,
1413    ///     quote::{QuoteContext, RequestUpdateWatchlistGroup},
1414    /// };
1415    ///
1416    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1417    /// let config = Arc::new(Config::from_env()?);
1418    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1419    /// let req = RequestUpdateWatchlistGroup::new(10086)
1420    ///     .name("Watchlist2")
1421    ///     .securities(["700.HK", "BABA.US"]);
1422    /// ctx.update_watchlist_group(req).await?;
1423    /// # Ok::<_, Box<dyn std::error::Error>>(())
1424    /// # });
1425    /// ```
1426    pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1427        #[derive(Debug, Serialize)]
1428        struct RequestUpdate {
1429            id: i64,
1430            #[serde(skip_serializing_if = "Option::is_none")]
1431            name: Option<String>,
1432            #[serde(skip_serializing_if = "Option::is_none")]
1433            securities: Option<Vec<String>>,
1434            #[serde(skip_serializing_if = "Option::is_none")]
1435            mode: Option<SecuritiesUpdateMode>,
1436        }
1437
1438        self.0
1439            .http_cli
1440            .request(Method::PUT, "/v1/watchlist/groups")
1441            .body(Json(RequestUpdate {
1442                id: req.id,
1443                name: req.name,
1444                mode: req.securities.is_some().then_some(req.mode),
1445                securities: req.securities,
1446            }))
1447            .send()
1448            .with_subscriber(self.0.log_subscriber.clone())
1449            .await?;
1450
1451        Ok(())
1452    }
1453
1454    /// Get security list
1455    pub async fn security_list(
1456        &self,
1457        market: Market,
1458        category: impl Into<Option<SecurityListCategory>>,
1459    ) -> Result<Vec<Security>> {
1460        #[derive(Debug, Serialize)]
1461        struct Request {
1462            market: Market,
1463            #[serde(skip_serializing_if = "Option::is_none")]
1464            category: Option<SecurityListCategory>,
1465        }
1466
1467        #[derive(Debug, Deserialize)]
1468        struct Response {
1469            list: Vec<Security>,
1470        }
1471
1472        Ok(self
1473            .0
1474            .http_cli
1475            .request(Method::GET, "/v1/quote/get_security_list")
1476            .query_params(Request {
1477                market,
1478                category: category.into(),
1479            })
1480            .response::<Json<Response>>()
1481            .send()
1482            .with_subscriber(self.0.log_subscriber.clone())
1483            .await?
1484            .0
1485            .list)
1486    }
1487
1488    /// Get current market temperature
1489    ///
1490    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/market_temperature>
1491    ///
1492    /// # Examples
1493    ///
1494    /// ```no_run
1495    /// use std::sync::Arc;
1496    ///
1497    /// use longport::{Config, Market, quote::QuoteContext};
1498    ///
1499    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1500    /// let config = Arc::new(Config::from_env()?);
1501    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1502    ///
1503    /// let resp = ctx.market_temperature(Market::HK).await?;
1504    /// println!("{:?}", resp);
1505    /// # Ok::<_, Box<dyn std::error::Error>>(())
1506    /// # });
1507    /// ```
1508    pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1509        #[derive(Debug, Serialize)]
1510        struct Request {
1511            market: Market,
1512        }
1513
1514        Ok(self
1515            .0
1516            .http_cli
1517            .request(Method::GET, "/v1/quote/market_temperature")
1518            .query_params(Request { market })
1519            .response::<Json<MarketTemperature>>()
1520            .send()
1521            .with_subscriber(self.0.log_subscriber.clone())
1522            .await?
1523            .0)
1524    }
1525
1526    /// Get historical market temperature
1527    ///
1528    /// Reference: <https://open.longportapp.com/en/docs/quote/pull/history_market_temperature>
1529    ///
1530    /// # Examples
1531    ///
1532    /// ```no_run
1533    /// use std::sync::Arc;
1534    ///
1535    /// use longport::{Config, Market, quote::QuoteContext};
1536    /// use time::macros::date;
1537    ///
1538    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1539    /// let config = Arc::new(Config::from_env()?);
1540    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1541    ///
1542    /// let resp = ctx
1543    ///     .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1544    ///     .await?;
1545    /// println!("{:?}", resp);
1546    /// # Ok::<_, Box<dyn std::error::Error>>(())
1547    /// # });
1548    /// ```
1549    pub async fn history_market_temperature(
1550        &self,
1551        market: Market,
1552        start_date: Date,
1553        end_date: Date,
1554    ) -> Result<HistoryMarketTemperatureResponse> {
1555        #[derive(Debug, Serialize)]
1556        struct Request {
1557            market: Market,
1558            start_date: String,
1559            end_date: String,
1560        }
1561
1562        Ok(self
1563            .0
1564            .http_cli
1565            .request(Method::GET, "/v1/quote/history_market_temperature")
1566            .query_params(Request {
1567                market,
1568                start_date: format_date(start_date),
1569                end_date: format_date(end_date),
1570            })
1571            .response::<Json<HistoryMarketTemperatureResponse>>()
1572            .send()
1573            .with_subscriber(self.0.log_subscriber.clone())
1574            .await?
1575            .0)
1576    }
1577
1578    /// Get real-time quotes
1579    ///
1580    /// Get real-time quotes of the subscribed symbols, it always returns the
1581    /// data in the local storage.
1582    ///
1583    /// # Examples
1584    ///
1585    /// ```no_run
1586    /// use std::{sync::Arc, time::Duration};
1587    ///
1588    /// use longport::{
1589    ///     Config,
1590    ///     quote::{QuoteContext, SubFlags},
1591    /// };
1592    ///
1593    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1594    /// let config = Arc::new(Config::from_env()?);
1595    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1596    ///
1597    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE, true)
1598    ///     .await?;
1599    /// tokio::time::sleep(Duration::from_secs(5)).await;
1600    ///
1601    /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
1602    /// println!("{:?}", resp);
1603    /// # Ok::<_, Box<dyn std::error::Error>>(())
1604    /// # });
1605    /// ```
1606    pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
1607    where
1608        I: IntoIterator<Item = T>,
1609        T: Into<String>,
1610    {
1611        let (reply_tx, reply_rx) = oneshot::channel();
1612        self.0
1613            .command_tx
1614            .send(Command::GetRealtimeQuote {
1615                symbols: symbols.into_iter().map(Into::into).collect(),
1616                reply_tx,
1617            })
1618            .map_err(|_| WsClientError::ClientClosed)?;
1619        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1620    }
1621
1622    /// Get real-time depth
1623    ///
1624    /// Get real-time depth of the subscribed symbols, it always returns the
1625    /// data in the local storage.
1626    ///
1627    /// # Examples
1628    ///
1629    /// ```no_run
1630    /// use std::{sync::Arc, time::Duration};
1631    ///
1632    /// use longport::{
1633    ///     Config,
1634    ///     quote::{QuoteContext, SubFlags},
1635    /// };
1636    ///
1637    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1638    /// let config = Arc::new(Config::from_env()?);
1639    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1640    ///
1641    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH, true)
1642    ///     .await?;
1643    /// tokio::time::sleep(Duration::from_secs(5)).await;
1644    ///
1645    /// let resp = ctx.realtime_depth("700.HK").await?;
1646    /// println!("{:?}", resp);
1647    /// # Ok::<_, Box<dyn std::error::Error>>(())
1648    /// # });
1649    /// ```
1650    pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
1651        let (reply_tx, reply_rx) = oneshot::channel();
1652        self.0
1653            .command_tx
1654            .send(Command::GetRealtimeDepth {
1655                symbol: symbol.into(),
1656                reply_tx,
1657            })
1658            .map_err(|_| WsClientError::ClientClosed)?;
1659        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1660    }
1661
1662    /// Get real-time trades
1663    ///
1664    /// Get real-time trades of the subscribed symbols, it always returns the
1665    /// data in the local storage.
1666    ///
1667    /// # Examples
1668    ///
1669    /// ```no_run
1670    /// use std::{sync::Arc, time::Duration};
1671    ///
1672    /// use longport::{
1673    ///     Config,
1674    ///     quote::{QuoteContext, SubFlags},
1675    /// };
1676    ///
1677    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1678    /// let config = Arc::new(Config::from_env()?);
1679    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1680    ///
1681    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE, false)
1682    ///     .await?;
1683    /// tokio::time::sleep(Duration::from_secs(5)).await;
1684    ///
1685    /// let resp = ctx.realtime_trades("700.HK", 10).await?;
1686    /// println!("{:?}", resp);
1687    /// # Ok::<_, Box<dyn std::error::Error>>(())
1688    /// # });
1689    /// ```
1690    pub async fn realtime_trades(
1691        &self,
1692        symbol: impl Into<String>,
1693        count: usize,
1694    ) -> Result<Vec<Trade>> {
1695        let (reply_tx, reply_rx) = oneshot::channel();
1696        self.0
1697            .command_tx
1698            .send(Command::GetRealtimeTrade {
1699                symbol: symbol.into(),
1700                count,
1701                reply_tx,
1702            })
1703            .map_err(|_| WsClientError::ClientClosed)?;
1704        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1705    }
1706
1707    /// Get real-time broker queue
1708    ///
1709    ///
1710    /// Get real-time broker queue of the subscribed symbols, it always returns
1711    /// the data in the local storage.
1712    ///
1713    /// # Examples
1714    ///
1715    /// ```no_run
1716    /// use std::{sync::Arc, time::Duration};
1717    ///
1718    /// use longport::{
1719    ///     Config,
1720    ///     quote::{QuoteContext, SubFlags},
1721    /// };
1722    ///
1723    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1724    /// let config = Arc::new(Config::from_env()?);
1725    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1726    ///
1727    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER, true)
1728    ///     .await?;
1729    /// tokio::time::sleep(Duration::from_secs(5)).await;
1730    ///
1731    /// let resp = ctx.realtime_brokers("700.HK").await?;
1732    /// println!("{:?}", resp);
1733    /// # Ok::<_, Box<dyn std::error::Error>>(())
1734    /// # });
1735    /// ```
1736    pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
1737        let (reply_tx, reply_rx) = oneshot::channel();
1738        self.0
1739            .command_tx
1740            .send(Command::GetRealtimeBrokers {
1741                symbol: symbol.into(),
1742                reply_tx,
1743            })
1744            .map_err(|_| WsClientError::ClientClosed)?;
1745        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1746    }
1747
1748    /// Get real-time candlesticks
1749    ///
1750    /// Get real-time candlesticks of the subscribed symbols, it always returns
1751    /// the data in the local storage.
1752    ///
1753    /// # Examples
1754    ///
1755    /// ```no_run
1756    /// use std::{sync::Arc, time::Duration};
1757    ///
1758    /// use longport::{
1759    ///     Config,
1760    ///     quote::{Period, QuoteContext, TradeSessions},
1761    /// };
1762    ///
1763    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1764    /// let config = Arc::new(Config::from_env()?);
1765    /// let (ctx, _) = QuoteContext::try_new(config).await?;
1766    ///
1767    /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
1768    ///     .await?;
1769    /// tokio::time::sleep(Duration::from_secs(5)).await;
1770    ///
1771    /// let resp = ctx
1772    ///     .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
1773    ///     .await?;
1774    /// println!("{:?}", resp);
1775    /// # Ok::<_, Box<dyn std::error::Error>>(())
1776    /// # });
1777    /// ```
1778    pub async fn realtime_candlesticks(
1779        &self,
1780        symbol: impl Into<String>,
1781        period: Period,
1782        count: usize,
1783    ) -> Result<Vec<Candlestick>> {
1784        let (reply_tx, reply_rx) = oneshot::channel();
1785        self.0
1786            .command_tx
1787            .send(Command::GetRealtimeCandlesticks {
1788                symbol: symbol.into(),
1789                period,
1790                count,
1791                reply_tx,
1792            })
1793            .map_err(|_| WsClientError::ClientClosed)?;
1794        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1795    }
1796}
1797
1798fn normalize_symbol(symbol: &str) -> &str {
1799    match symbol.split_once('.') {
1800        Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
1801        _ => symbol,
1802    }
1803}