Skip to main content

longbridge/quote/
context.rs

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