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