Skip to main content

longport/quote/
context.rs

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