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::{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, OptionVolumeStats,
21        ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote,
22        RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers,
23        SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo,
24        ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse,
25        StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote,
26        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        let resp: quote::SecurityBrokersResponse = self
672            .request(
673                cmd_code::GET_SECURITY_BROKERS,
674                quote::SecurityRequest {
675                    symbol: symbol.into(),
676                },
677            )
678            .await?;
679        Ok(SecurityBrokers {
680            ask_brokers: resp.ask_brokers.into_iter().map(Into::into).collect(),
681            bid_brokers: resp.bid_brokers.into_iter().map(Into::into).collect(),
682        })
683    }
684
685    /// Get participants
686    ///
687    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/broker-ids>
688    ///
689    /// # Examples
690    ///
691    /// ```no_run
692    /// use std::sync::Arc;
693    ///
694    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
695    ///
696    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
697    /// let oauth = OAuthBuilder::new("your-client-id")
698    ///     .build(|url| println!("Visit: {url}"))
699    ///     .await?;
700    /// let config = Arc::new(Config::from_oauth(oauth));
701    /// let (ctx, _) = QuoteContext::new(config);
702    ///
703    /// let resp = ctx.participants().await?;
704    /// println!("{:?}", resp);
705    /// # Ok::<_, Box<dyn std::error::Error>>(())
706    /// # });
707    /// ```
708    pub async fn participants(&self) -> Result<Vec<ParticipantInfo>> {
709        self.0
710            .cache_participants
711            .get_or_update(|| async {
712                let resp = self
713                    .request_without_body::<quote::ParticipantBrokerIdsResponse>(
714                        cmd_code::GET_BROKER_IDS,
715                    )
716                    .await?;
717
718                Ok(resp
719                    .participant_broker_numbers
720                    .into_iter()
721                    .map(Into::into)
722                    .collect())
723            })
724            .await
725    }
726
727    /// Get security trades
728    ///
729    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade>
730    ///
731    /// # Examples
732    ///
733    /// ```no_run
734    /// use std::sync::Arc;
735    ///
736    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
737    ///
738    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
739    /// let oauth = OAuthBuilder::new("your-client-id")
740    ///     .build(|url| println!("Visit: {url}"))
741    ///     .await?;
742    /// let config = Arc::new(Config::from_oauth(oauth));
743    /// let (ctx, _) = QuoteContext::new(config);
744    ///
745    /// let resp = ctx.trades("700.HK", 10).await?;
746    /// println!("{:?}", resp);
747    /// # Ok::<_, Box<dyn std::error::Error>>(())
748    /// # });
749    /// ```
750    pub async fn trades(&self, symbol: impl Into<String>, count: usize) -> Result<Vec<Trade>> {
751        let resp: quote::SecurityTradeResponse = self
752            .request(
753                cmd_code::GET_SECURITY_TRADES,
754                quote::SecurityTradeRequest {
755                    symbol: symbol.into(),
756                    count: count as i32,
757                },
758            )
759            .await?;
760        let trades = resp
761            .trades
762            .into_iter()
763            .map(TryInto::try_into)
764            .collect::<Result<Vec<_>>>()?;
765        Ok(trades)
766    }
767
768    /// Get security intraday lines
769    ///
770    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/intraday>
771    ///
772    /// # Examples
773    ///
774    /// ```no_run
775    /// use std::sync::Arc;
776    ///
777    /// use longbridge::{
778    ///     Config,
779    ///     oauth::OAuthBuilder,
780    ///     quote::{QuoteContext, TradeSessions},
781    /// };
782    ///
783    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
784    /// let oauth = OAuthBuilder::new("your-client-id")
785    ///     .build(|url| println!("Visit: {url}"))
786    ///     .await?;
787    /// let config = Arc::new(Config::from_oauth(oauth));
788    /// let (ctx, _) = QuoteContext::new(config);
789    ///
790    /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
791    /// println!("{:?}", resp);
792    /// # Ok::<_, Box<dyn std::error::Error>>(())
793    /// # });
794    /// ```
795    pub async fn intraday(
796        &self,
797        symbol: impl Into<String>,
798        trade_sessions: TradeSessions,
799    ) -> Result<Vec<IntradayLine>> {
800        let resp: quote::SecurityIntradayResponse = self
801            .request(
802                cmd_code::GET_SECURITY_INTRADAY,
803                quote::SecurityIntradayRequest {
804                    symbol: symbol.into(),
805                    trade_session: trade_sessions as i32,
806                },
807            )
808            .await?;
809        let lines = resp
810            .lines
811            .into_iter()
812            .map(TryInto::try_into)
813            .collect::<Result<Vec<_>>>()?;
814        Ok(lines)
815    }
816
817    /// Get security candlesticks
818    ///
819    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/candlestick>
820    ///
821    /// # Examples
822    ///
823    /// ```no_run
824    /// use std::sync::Arc;
825    ///
826    /// use longbridge::{
827    ///     Config,
828    ///     oauth::OAuthBuilder,
829    ///     quote::{AdjustType, Period, QuoteContext, TradeSessions},
830    /// };
831    ///
832    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
833    /// let oauth = OAuthBuilder::new("your-client-id")
834    ///     .build(|url| println!("Visit: {url}"))
835    ///     .await?;
836    /// let config = Arc::new(Config::from_oauth(oauth));
837    /// let (ctx, _) = QuoteContext::new(config);
838    ///
839    /// let resp = ctx
840    ///     .candlesticks(
841    ///         "700.HK",
842    ///         Period::Day,
843    ///         10,
844    ///         AdjustType::NoAdjust,
845    ///         TradeSessions::Intraday,
846    ///     )
847    ///     .await?;
848    /// println!("{:?}", resp);
849    /// # Ok::<_, Box<dyn std::error::Error>>(())
850    /// # });
851    /// ```
852    pub async fn candlesticks(
853        &self,
854        symbol: impl Into<String>,
855        period: Period,
856        count: usize,
857        adjust_type: AdjustType,
858        trade_sessions: TradeSessions,
859    ) -> Result<Vec<Candlestick>> {
860        let resp: quote::SecurityCandlestickResponse = self
861            .request(
862                cmd_code::GET_SECURITY_CANDLESTICKS,
863                quote::SecurityCandlestickRequest {
864                    symbol: symbol.into(),
865                    period: period.into(),
866                    count: count as i32,
867                    adjust_type: adjust_type.into(),
868                    trade_session: trade_sessions as i32,
869                },
870            )
871            .await?;
872        let candlesticks = resp
873            .candlesticks
874            .into_iter()
875            .map(TryInto::try_into)
876            .collect::<Result<Vec<_>>>()?;
877        Ok(candlesticks)
878    }
879
880    /// Get security history candlesticks by offset
881    #[allow(clippy::too_many_arguments)]
882    pub async fn history_candlesticks_by_offset(
883        &self,
884        symbol: impl Into<String>,
885        period: Period,
886        adjust_type: AdjustType,
887        forward: bool,
888        time: Option<PrimitiveDateTime>,
889        count: usize,
890        trade_sessions: TradeSessions,
891    ) -> Result<Vec<Candlestick>> {
892        let resp: quote::SecurityCandlestickResponse = self
893            .request(
894                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
895                quote::SecurityHistoryCandlestickRequest {
896                    symbol: symbol.into(),
897                    period: period.into(),
898                    adjust_type: adjust_type.into(),
899                    query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
900                    offset_request: Some(
901                        quote::security_history_candlestick_request::OffsetQuery {
902                            direction: if forward {
903                                quote::Direction::Forward
904                            } else {
905                                quote::Direction::Backward
906                            }
907                            .into(),
908                            date: time
909                                .map(|time| {
910                                    format!(
911                                        "{:04}{:02}{:02}",
912                                        time.year(),
913                                        time.month() as u8,
914                                        time.day()
915                                    )
916                                })
917                                .unwrap_or_default(),
918                            minute: time
919                                .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
920                                .unwrap_or_default(),
921                            count: count as i32,
922                        },
923                    ),
924                    date_request: None,
925                    trade_session: trade_sessions as i32,
926                },
927            )
928            .await?;
929        let candlesticks = resp
930            .candlesticks
931            .into_iter()
932            .map(TryInto::try_into)
933            .collect::<Result<Vec<_>>>()?;
934        Ok(candlesticks)
935    }
936
937    /// Get security history candlesticks by date
938    pub async fn history_candlesticks_by_date(
939        &self,
940        symbol: impl Into<String>,
941        period: Period,
942        adjust_type: AdjustType,
943        start: Option<Date>,
944        end: Option<Date>,
945        trade_sessions: TradeSessions,
946    ) -> Result<Vec<Candlestick>> {
947        let resp: quote::SecurityCandlestickResponse = self
948            .request(
949                cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
950                quote::SecurityHistoryCandlestickRequest {
951                    symbol: symbol.into(),
952                    period: period.into(),
953                    adjust_type: adjust_type.into(),
954                    query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
955                    offset_request: None,
956                    date_request: Some(quote::security_history_candlestick_request::DateQuery {
957                        start_date: start
958                            .map(|date| {
959                                format!(
960                                    "{:04}{:02}{:02}",
961                                    date.year(),
962                                    date.month() as u8,
963                                    date.day()
964                                )
965                            })
966                            .unwrap_or_default(),
967                        end_date: end
968                            .map(|date| {
969                                format!(
970                                    "{:04}{:02}{:02}",
971                                    date.year(),
972                                    date.month() as u8,
973                                    date.day()
974                                )
975                            })
976                            .unwrap_or_default(),
977                    }),
978                    trade_session: trade_sessions as i32,
979                },
980            )
981            .await?;
982        let candlesticks = resp
983            .candlesticks
984            .into_iter()
985            .map(TryInto::try_into)
986            .collect::<Result<Vec<_>>>()?;
987        Ok(candlesticks)
988    }
989
990    /// Get option chain expiry date list
991    ///
992    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date>
993    ///
994    /// # Examples
995    ///
996    /// ```no_run
997    /// use std::sync::Arc;
998    ///
999    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1000    ///
1001    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1002    /// let oauth = OAuthBuilder::new("your-client-id")
1003    ///     .build(|url| println!("Visit: {url}"))
1004    ///     .await?;
1005    /// let config = Arc::new(Config::from_oauth(oauth));
1006    /// let (ctx, _) = QuoteContext::new(config);
1007    ///
1008    /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
1009    /// println!("{:?}", resp);
1010    /// # Ok::<_, Box<dyn std::error::Error>>(())
1011    /// # });
1012    /// ```
1013    pub async fn option_chain_expiry_date_list(
1014        &self,
1015        symbol: impl Into<String>,
1016    ) -> Result<Vec<Date>> {
1017        self.0
1018            .cache_option_chain_expiry_date_list
1019            .get_or_update(symbol.into(), |symbol| async {
1020                let resp: quote::OptionChainDateListResponse = self
1021                    .request(
1022                        cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
1023                        quote::SecurityRequest { symbol },
1024                    )
1025                    .await?;
1026                resp.expiry_date
1027                    .iter()
1028                    .map(|value| {
1029                        parse_date(value).map_err(|err| Error::parse_field_error("date", err))
1030                    })
1031                    .collect::<Result<Vec<_>>>()
1032            })
1033            .await
1034    }
1035
1036    /// Get option chain info by date
1037    ///
1038    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date-strike>
1039    ///
1040    /// # Examples
1041    ///
1042    /// ```no_run
1043    /// use std::sync::Arc;
1044    ///
1045    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1046    /// use time::macros::date;
1047    ///
1048    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1049    /// let oauth = OAuthBuilder::new("your-client-id")
1050    ///     .build(|url| println!("Visit: {url}"))
1051    ///     .await?;
1052    /// let config = Arc::new(Config::from_oauth(oauth));
1053    /// let (ctx, _) = QuoteContext::new(config);
1054    ///
1055    /// let resp = ctx
1056    ///     .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))
1057    ///     .await?;
1058    /// println!("{:?}", resp);
1059    /// # Ok::<_, Box<dyn std::error::Error>>(())
1060    /// # });
1061    /// ```
1062    pub async fn option_chain_info_by_date(
1063        &self,
1064        symbol: impl Into<String>,
1065        expiry_date: Date,
1066    ) -> Result<Vec<StrikePriceInfo>> {
1067        self.0
1068            .cache_option_chain_strike_info
1069            .get_or_update(
1070                (symbol.into(), expiry_date),
1071                |(symbol, expiry_date)| async move {
1072                    let resp: quote::OptionChainDateStrikeInfoResponse = self
1073                        .request(
1074                            cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE,
1075                            quote::OptionChainDateStrikeInfoRequest {
1076                                symbol,
1077                                expiry_date: format_date(expiry_date),
1078                            },
1079                        )
1080                        .await?;
1081                    resp.strike_price_info
1082                        .into_iter()
1083                        .map(TryInto::try_into)
1084                        .collect::<Result<Vec<_>>>()
1085                },
1086            )
1087            .await
1088    }
1089
1090    /// Get warrant issuers
1091    ///
1092    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/issuer>
1093    ///
1094    /// # Examples
1095    ///
1096    /// ```no_run
1097    /// use std::sync::Arc;
1098    ///
1099    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1100    ///
1101    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1102    /// let oauth = OAuthBuilder::new("your-client-id")
1103    ///     .build(|url| println!("Visit: {url}"))
1104    ///     .await?;
1105    /// let config = Arc::new(Config::from_oauth(oauth));
1106    /// let (ctx, _) = QuoteContext::new(config);
1107    ///
1108    /// let resp = ctx.warrant_issuers().await?;
1109    /// println!("{:?}", resp);
1110    /// # Ok::<_, Box<dyn std::error::Error>>(())
1111    /// # });
1112    /// ```
1113    pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1114        self.0
1115            .cache_issuers
1116            .get_or_update(|| async {
1117                let resp = self
1118                    .request_without_body::<quote::IssuerInfoResponse>(
1119                        cmd_code::GET_WARRANT_ISSUER_IDS,
1120                    )
1121                    .await?;
1122                Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1123            })
1124            .await
1125    }
1126
1127    /// Query warrant list
1128    #[allow(clippy::too_many_arguments)]
1129    pub async fn warrant_list(
1130        &self,
1131        symbol: impl Into<String>,
1132        sort_by: WarrantSortBy,
1133        sort_order: SortOrderType,
1134        warrant_type: Option<&[WarrantType]>,
1135        issuer: Option<&[i32]>,
1136        expiry_date: Option<&[FilterWarrantExpiryDate]>,
1137        price_type: Option<&[FilterWarrantInOutBoundsType]>,
1138        status: Option<&[WarrantStatus]>,
1139    ) -> Result<Vec<WarrantInfo>> {
1140        let resp = self
1141            .request::<_, quote::WarrantFilterListResponse>(
1142                cmd_code::GET_FILTERED_WARRANT,
1143                quote::WarrantFilterListRequest {
1144                    symbol: symbol.into(),
1145                    filter_config: Some(quote::FilterConfig {
1146                        sort_by: sort_by.into(),
1147                        sort_order: sort_order.into(),
1148                        sort_offset: 0,
1149                        sort_count: 0,
1150                        r#type: warrant_type
1151                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1152                            .unwrap_or_default(),
1153                        issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1154                        expiry_date: expiry_date
1155                            .map(|e| e.iter().map(|e| (*e).into()).collect())
1156                            .unwrap_or_default(),
1157                        price_type: price_type
1158                            .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1159                            .unwrap_or_default(),
1160                        status: status
1161                            .map(|status| status.iter().map(|status| (*status).into()).collect())
1162                            .unwrap_or_default(),
1163                    }),
1164                    language: self.0.language.into(),
1165                },
1166            )
1167            .await?;
1168        resp.warrant_list
1169            .into_iter()
1170            .map(TryInto::try_into)
1171            .collect::<Result<Vec<_>>>()
1172    }
1173
1174    /// Get trading session of the day
1175    ///
1176    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-session>
1177    ///
1178    /// # Examples
1179    ///
1180    /// ```no_run
1181    /// use std::sync::Arc;
1182    ///
1183    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1184    ///
1185    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1186    /// let oauth = OAuthBuilder::new("your-client-id")
1187    ///     .build(|url| println!("Visit: {url}"))
1188    ///     .await?;
1189    /// let config = Arc::new(Config::from_oauth(oauth));
1190    /// let (ctx, _) = QuoteContext::new(config);
1191    ///
1192    /// let resp = ctx.trading_session().await?;
1193    /// println!("{:?}", resp);
1194    /// # Ok::<_, Box<dyn std::error::Error>>(())
1195    /// # });
1196    /// ```
1197    pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1198        self.0
1199            .cache_trading_session
1200            .get_or_update(|| async {
1201                let resp = self
1202                    .request_without_body::<quote::MarketTradePeriodResponse>(
1203                        cmd_code::GET_TRADING_SESSION,
1204                    )
1205                    .await?;
1206                resp.market_trade_session
1207                    .into_iter()
1208                    .map(TryInto::try_into)
1209                    .collect::<Result<Vec<_>>>()
1210            })
1211            .await
1212    }
1213
1214    /// Get market trading days
1215    ///
1216    /// The interval must be less than one month, and only the most recent year
1217    /// is supported.
1218    ///
1219    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-day>
1220    ///
1221    /// # Examples
1222    ///
1223    /// ```no_run
1224    /// use std::sync::Arc;
1225    ///
1226    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1227    /// use time::macros::date;
1228    ///
1229    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1230    /// let oauth = OAuthBuilder::new("your-client-id")
1231    ///     .build(|url| println!("Visit: {url}"))
1232    ///     .await?;
1233    /// let config = Arc::new(Config::from_oauth(oauth));
1234    /// let (ctx, _) = QuoteContext::new(config);
1235    ///
1236    /// let resp = ctx
1237    ///     .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1238    ///     .await?;
1239    /// println!("{:?}", resp);
1240    /// # Ok::<_, Box<dyn std::error::Error>>(())
1241    /// # });
1242    /// ```
1243    pub async fn trading_days(
1244        &self,
1245        market: Market,
1246        begin: Date,
1247        end: Date,
1248    ) -> Result<MarketTradingDays> {
1249        let resp = self
1250            .request::<_, quote::MarketTradeDayResponse>(
1251                cmd_code::GET_TRADING_DAYS,
1252                quote::MarketTradeDayRequest {
1253                    market: market.to_string(),
1254                    beg_day: format_date(begin),
1255                    end_day: format_date(end),
1256                },
1257            )
1258            .await?;
1259        let trading_days = resp
1260            .trade_day
1261            .iter()
1262            .map(|value| {
1263                parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1264            })
1265            .collect::<Result<Vec<_>>>()?;
1266        let half_trading_days = resp
1267            .half_trade_day
1268            .iter()
1269            .map(|value| {
1270                parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1271            })
1272            .collect::<Result<Vec<_>>>()?;
1273        Ok(MarketTradingDays {
1274            trading_days,
1275            half_trading_days,
1276        })
1277    }
1278
1279    /// Get capital flow intraday
1280    ///
1281    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-flow-intraday>
1282    ///
1283    /// # Examples
1284    ///
1285    /// ```no_run
1286    /// use std::sync::Arc;
1287    ///
1288    /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1289    ///
1290    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1291    /// let oauth = OAuthBuilder::new("your-client-id")
1292    ///     .build(|url| println!("Visit: {url}"))
1293    ///     .await?;
1294    /// let config = Arc::new(Config::from_oauth(oauth));
1295    /// let (ctx, _) = QuoteContext::new(config);
1296    ///
1297    /// let resp = ctx.capital_flow("700.HK").await?;
1298    /// println!("{:?}", resp);
1299    /// # Ok::<_, Box<dyn std::error::Error>>(())
1300    /// # });
1301    pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1302        self.request::<_, quote::CapitalFlowIntradayResponse>(
1303            cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1304            quote::CapitalFlowIntradayRequest {
1305                symbol: symbol.into(),
1306            },
1307        )
1308        .await?
1309        .capital_flow_lines
1310        .into_iter()
1311        .map(TryInto::try_into)
1312        .collect()
1313    }
1314
1315    /// Get capital distribution
1316    ///
1317    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-distribution>
1318    ///
1319    /// # Examples
1320    ///
1321    /// ```no_run
1322    /// use std::sync::Arc;
1323    ///
1324    /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1325    ///
1326    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1327    /// let oauth = OAuthBuilder::new("your-client-id")
1328    ///     .build(|url| println!("Visit: {url}"))
1329    ///     .await?;
1330    /// let config = Arc::new(Config::from_oauth(oauth));
1331    /// let (ctx, _) = QuoteContext::new(config);
1332    ///
1333    /// let resp = ctx.capital_distribution("700.HK").await?;
1334    /// println!("{:?}", resp);
1335    /// # Ok::<_, Box<dyn std::error::Error>>(())
1336    /// # });
1337    pub async fn capital_distribution(
1338        &self,
1339        symbol: impl Into<String>,
1340    ) -> Result<CapitalDistributionResponse> {
1341        self.request::<_, quote::CapitalDistributionResponse>(
1342            cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1343            quote::SecurityRequest {
1344                symbol: symbol.into(),
1345            },
1346        )
1347        .await?
1348        .try_into()
1349    }
1350
1351    /// Get calc indexes
1352    pub async fn calc_indexes<I, T, J>(
1353        &self,
1354        symbols: I,
1355        indexes: J,
1356    ) -> Result<Vec<SecurityCalcIndex>>
1357    where
1358        I: IntoIterator<Item = T>,
1359        T: Into<String>,
1360        J: IntoIterator<Item = CalcIndex>,
1361    {
1362        let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1363        let resp: quote::SecurityCalcQuoteResponse = self
1364            .request(
1365                cmd_code::GET_CALC_INDEXES,
1366                quote::SecurityCalcQuoteRequest {
1367                    symbols: symbols.into_iter().map(Into::into).collect(),
1368                    calc_index: indexes
1369                        .iter()
1370                        .map(|i| quote::CalcIndex::from(*i).into())
1371                        .collect(),
1372                },
1373            )
1374            .await?;
1375
1376        Ok(resp
1377            .security_calc_index
1378            .into_iter()
1379            .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1380            .collect())
1381    }
1382
1383    /// Get watchlist
1384    ///
1385    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_groups>
1386    ///
1387    /// # Examples
1388    ///
1389    /// ```no_run
1390    /// use std::sync::Arc;
1391    ///
1392    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1393    ///
1394    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1395    /// let oauth = OAuthBuilder::new("your-client-id")
1396    ///     .build(|url| println!("Visit: {url}"))
1397    ///     .await?;
1398    /// let config = Arc::new(Config::from_oauth(oauth));
1399    /// let (ctx, _) = QuoteContext::new(config);
1400    ///
1401    /// let resp = ctx.watchlist().await?;
1402    /// println!("{:?}", resp);
1403    /// # Ok::<_, Box<dyn std::error::Error>>(())
1404    /// # });
1405    /// ```
1406    pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1407        #[derive(Debug, Deserialize)]
1408        struct Response {
1409            groups: Vec<WatchlistGroup>,
1410        }
1411
1412        let resp = self
1413            .0
1414            .http_cli
1415            .request(Method::GET, "/v1/watchlist/groups")
1416            .response::<Json<Response>>()
1417            .send()
1418            .with_subscriber(self.0.log_subscriber.clone())
1419            .await?;
1420        Ok(resp.0.groups)
1421    }
1422
1423    /// Create watchlist group
1424    ///
1425    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_create_group>
1426    ///
1427    /// # Examples
1428    ///
1429    /// ```no_run
1430    /// use std::sync::Arc;
1431    ///
1432    /// use longbridge::{
1433    ///     Config,
1434    ///     oauth::OAuthBuilder,
1435    ///     quote::{QuoteContext, RequestCreateWatchlistGroup},
1436    /// };
1437    ///
1438    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1439    /// let oauth = OAuthBuilder::new("your-client-id")
1440    ///     .build(|url| println!("Visit: {url}"))
1441    ///     .await?;
1442    /// let config = Arc::new(Config::from_oauth(oauth));
1443    /// let (ctx, _) = QuoteContext::new(config);
1444    ///
1445    /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1446    /// let group_id = ctx.create_watchlist_group(req).await?;
1447    /// println!("{}", group_id);
1448    /// # Ok::<_, Box<dyn std::error::Error>>(())
1449    /// # });
1450    /// ```
1451    pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1452        #[derive(Debug, Serialize)]
1453        struct RequestCreate {
1454            name: String,
1455            #[serde(skip_serializing_if = "Option::is_none")]
1456            securities: Option<Vec<String>>,
1457        }
1458
1459        #[derive(Debug, Deserialize)]
1460        struct Response {
1461            #[serde(with = "serde_utils::int64_str")]
1462            id: i64,
1463        }
1464
1465        let Json(Response { id }) = self
1466            .0
1467            .http_cli
1468            .request(Method::POST, "/v1/watchlist/groups")
1469            .body(Json(RequestCreate {
1470                name: req.name,
1471                securities: req.securities,
1472            }))
1473            .response::<Json<Response>>()
1474            .send()
1475            .with_subscriber(self.0.log_subscriber.clone())
1476            .await?;
1477
1478        Ok(id)
1479    }
1480
1481    /// Delete watchlist group
1482    ///
1483    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_delete_group>
1484    ///
1485    /// # Examples
1486    ///
1487    /// ```no_run
1488    /// use std::sync::Arc;
1489    ///
1490    /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1491    ///
1492    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1493    /// let oauth = OAuthBuilder::new("your-client-id")
1494    ///     .build(|url| println!("Visit: {url}"))
1495    ///     .await?;
1496    /// let config = Arc::new(Config::from_oauth(oauth));
1497    /// let (ctx, _) = QuoteContext::new(config);
1498    ///
1499    /// ctx.delete_watchlist_group(10086, true).await?;
1500    /// # Ok::<_, Box<dyn std::error::Error>>(())
1501    /// # });
1502    /// ```
1503    pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1504        #[derive(Debug, Serialize)]
1505        struct Request {
1506            id: i64,
1507            purge: bool,
1508        }
1509
1510        Ok(self
1511            .0
1512            .http_cli
1513            .request(Method::DELETE, "/v1/watchlist/groups")
1514            .query_params(Request { id, purge })
1515            .send()
1516            .with_subscriber(self.0.log_subscriber.clone())
1517            .await?)
1518    }
1519
1520    /// Update watchlist group
1521    ///
1522    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group>
1523    /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group_securities>
1524    ///
1525    /// # Examples
1526    ///
1527    /// ```no_run
1528    /// use std::sync::Arc;
1529    ///
1530    /// use longbridge::{
1531    ///     Config,
1532    ///     oauth::OAuthBuilder,
1533    ///     quote::{QuoteContext, RequestUpdateWatchlistGroup},
1534    /// };
1535    ///
1536    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1537    /// let oauth = OAuthBuilder::new("your-client-id")
1538    ///     .build(|url| println!("Visit: {url}"))
1539    ///     .await?;
1540    /// let config = Arc::new(Config::from_oauth(oauth));
1541    /// let (ctx, _) = QuoteContext::new(config);
1542    /// let req = RequestUpdateWatchlistGroup::new(10086)
1543    ///     .name("Watchlist2")
1544    ///     .securities(["700.HK", "BABA.US"]);
1545    /// ctx.update_watchlist_group(req).await?;
1546    /// # Ok::<_, Box<dyn std::error::Error>>(())
1547    /// # });
1548    /// ```
1549    pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1550        #[derive(Debug, Serialize)]
1551        struct RequestUpdate {
1552            id: i64,
1553            #[serde(skip_serializing_if = "Option::is_none")]
1554            name: Option<String>,
1555            #[serde(skip_serializing_if = "Option::is_none")]
1556            securities: Option<Vec<String>>,
1557            #[serde(skip_serializing_if = "Option::is_none")]
1558            mode: Option<SecuritiesUpdateMode>,
1559        }
1560
1561        self.0
1562            .http_cli
1563            .request(Method::PUT, "/v1/watchlist/groups")
1564            .body(Json(RequestUpdate {
1565                id: req.id,
1566                name: req.name,
1567                mode: req.securities.is_some().then_some(req.mode),
1568                securities: req.securities,
1569            }))
1570            .send()
1571            .with_subscriber(self.0.log_subscriber.clone())
1572            .await?;
1573
1574        Ok(())
1575    }
1576
1577    /// Get security list
1578    pub async fn security_list(
1579        &self,
1580        market: Market,
1581        category: impl Into<Option<SecurityListCategory>>,
1582    ) -> Result<Vec<Security>> {
1583        #[derive(Debug, Serialize)]
1584        struct Request {
1585            market: Market,
1586            #[serde(skip_serializing_if = "Option::is_none")]
1587            category: Option<SecurityListCategory>,
1588        }
1589
1590        #[derive(Debug, Deserialize)]
1591        struct Response {
1592            list: Vec<Security>,
1593        }
1594
1595        Ok(self
1596            .0
1597            .http_cli
1598            .request(Method::GET, "/v1/quote/get_security_list")
1599            .query_params(Request {
1600                market,
1601                category: category.into(),
1602            })
1603            .response::<Json<Response>>()
1604            .send()
1605            .with_subscriber(self.0.log_subscriber.clone())
1606            .await?
1607            .0
1608            .list)
1609    }
1610
1611    /// Get filings list
1612    pub async fn filings(&self, symbol: impl Into<String>) -> Result<Vec<FilingItem>> {
1613        #[derive(Debug, Serialize)]
1614        struct Request {
1615            symbol: String,
1616        }
1617
1618        #[derive(Debug, Deserialize)]
1619        struct Response {
1620            items: Vec<FilingItem>,
1621        }
1622
1623        Ok(self
1624            .0
1625            .http_cli
1626            .request(Method::GET, "/v1/quote/filings")
1627            .query_params(Request {
1628                symbol: symbol.into(),
1629            })
1630            .response::<Json<Response>>()
1631            .send()
1632            .with_subscriber(self.0.log_subscriber.clone())
1633            .await?
1634            .0
1635            .items)
1636    }
1637
1638    /// Get current market temperature
1639    ///
1640    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/market_temperature>
1641    ///
1642    /// # Examples
1643    ///
1644    /// ```no_run
1645    /// use std::sync::Arc;
1646    ///
1647    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1648    ///
1649    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1650    /// let oauth = OAuthBuilder::new("your-client-id")
1651    ///     .build(|url| println!("Visit: {url}"))
1652    ///     .await?;
1653    /// let config = Arc::new(Config::from_oauth(oauth));
1654    /// let (ctx, _) = QuoteContext::new(config);
1655    ///
1656    /// let resp = ctx.market_temperature(Market::HK).await?;
1657    /// println!("{:?}", resp);
1658    /// # Ok::<_, Box<dyn std::error::Error>>(())
1659    /// # });
1660    /// ```
1661    pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1662        #[derive(Debug, Serialize)]
1663        struct Request {
1664            market: Market,
1665        }
1666
1667        Ok(self
1668            .0
1669            .http_cli
1670            .request(Method::GET, "/v1/quote/market_temperature")
1671            .query_params(Request { market })
1672            .response::<Json<MarketTemperature>>()
1673            .send()
1674            .with_subscriber(self.0.log_subscriber.clone())
1675            .await?
1676            .0)
1677    }
1678
1679    /// Get historical market temperature
1680    ///
1681    /// Reference: <https://open.longbridge.com/en/docs/quote/pull/history_market_temperature>
1682    ///
1683    /// # Examples
1684    ///
1685    /// ```no_run
1686    /// use std::sync::Arc;
1687    ///
1688    /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1689    /// use time::macros::date;
1690    ///
1691    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1692    /// let oauth = OAuthBuilder::new("your-client-id")
1693    ///     .build(|url| println!("Visit: {url}"))
1694    ///     .await?;
1695    /// let config = Arc::new(Config::from_oauth(oauth));
1696    /// let (ctx, _) = QuoteContext::new(config);
1697    ///
1698    /// let resp = ctx
1699    ///     .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1700    ///     .await?;
1701    /// println!("{:?}", resp);
1702    /// # Ok::<_, Box<dyn std::error::Error>>(())
1703    /// # });
1704    /// ```
1705    pub async fn history_market_temperature(
1706        &self,
1707        market: Market,
1708        start_date: Date,
1709        end_date: Date,
1710    ) -> Result<HistoryMarketTemperatureResponse> {
1711        #[derive(Debug, Serialize)]
1712        struct Request {
1713            market: Market,
1714            start_date: String,
1715            end_date: String,
1716        }
1717
1718        Ok(self
1719            .0
1720            .http_cli
1721            .request(Method::GET, "/v1/quote/history_market_temperature")
1722            .query_params(Request {
1723                market,
1724                start_date: format_date(start_date),
1725                end_date: format_date(end_date),
1726            })
1727            .response::<Json<HistoryMarketTemperatureResponse>>()
1728            .send()
1729            .with_subscriber(self.0.log_subscriber.clone())
1730            .await?
1731            .0)
1732    }
1733
1734    /// Get real-time quotes
1735    ///
1736    /// Get real-time quotes of the subscribed symbols, it always returns the
1737    /// data in the local storage.
1738    ///
1739    /// # Examples
1740    ///
1741    /// ```no_run
1742    /// use std::{sync::Arc, time::Duration};
1743    ///
1744    /// use longbridge::{
1745    ///     Config,
1746    ///     oauth::OAuthBuilder,
1747    ///     quote::{QuoteContext, SubFlags},
1748    /// };
1749    ///
1750    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1751    /// let oauth = OAuthBuilder::new("your-client-id")
1752    ///     .build(|url| println!("Visit: {url}"))
1753    ///     .await?;
1754    /// let config = Arc::new(Config::from_oauth(oauth));
1755    /// let (ctx, _) = QuoteContext::new(config);
1756    ///
1757    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
1758    ///     .await?;
1759    /// tokio::time::sleep(Duration::from_secs(5)).await;
1760    ///
1761    /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
1762    /// println!("{:?}", resp);
1763    /// # Ok::<_, Box<dyn std::error::Error>>(())
1764    /// # });
1765    /// ```
1766    pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
1767    where
1768        I: IntoIterator<Item = T>,
1769        T: Into<String>,
1770    {
1771        let (reply_tx, reply_rx) = oneshot::channel();
1772        self.0
1773            .command_tx
1774            .send(Command::GetRealtimeQuote {
1775                symbols: symbols.into_iter().map(Into::into).collect(),
1776                reply_tx,
1777            })
1778            .map_err(|_| WsClientError::ClientClosed)?;
1779        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1780    }
1781
1782    /// Get real-time depth
1783    ///
1784    /// Get real-time depth of the subscribed symbols, it always returns the
1785    /// data in the local storage.
1786    ///
1787    /// # Examples
1788    ///
1789    /// ```no_run
1790    /// use std::{sync::Arc, time::Duration};
1791    ///
1792    /// use longbridge::{
1793    ///     Config,
1794    ///     oauth::OAuthBuilder,
1795    ///     quote::{QuoteContext, SubFlags},
1796    /// };
1797    ///
1798    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1799    /// let oauth = OAuthBuilder::new("your-client-id")
1800    ///     .build(|url| println!("Visit: {url}"))
1801    ///     .await?;
1802    /// let config = Arc::new(Config::from_oauth(oauth));
1803    /// let (ctx, _) = QuoteContext::new(config);
1804    ///
1805    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH)
1806    ///     .await?;
1807    /// tokio::time::sleep(Duration::from_secs(5)).await;
1808    ///
1809    /// let resp = ctx.realtime_depth("700.HK").await?;
1810    /// println!("{:?}", resp);
1811    /// # Ok::<_, Box<dyn std::error::Error>>(())
1812    /// # });
1813    /// ```
1814    pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
1815        let (reply_tx, reply_rx) = oneshot::channel();
1816        self.0
1817            .command_tx
1818            .send(Command::GetRealtimeDepth {
1819                symbol: symbol.into(),
1820                reply_tx,
1821            })
1822            .map_err(|_| WsClientError::ClientClosed)?;
1823        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1824    }
1825
1826    /// Get real-time trades
1827    ///
1828    /// Get real-time trades of the subscribed symbols, it always returns the
1829    /// data in the local storage.
1830    ///
1831    /// # Examples
1832    ///
1833    /// ```no_run
1834    /// use std::{sync::Arc, time::Duration};
1835    ///
1836    /// use longbridge::{
1837    ///     Config,
1838    ///     oauth::OAuthBuilder,
1839    ///     quote::{QuoteContext, SubFlags},
1840    /// };
1841    ///
1842    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1843    /// let oauth = OAuthBuilder::new("your-client-id")
1844    ///     .build(|url| println!("Visit: {url}"))
1845    ///     .await?;
1846    /// let config = Arc::new(Config::from_oauth(oauth));
1847    /// let (ctx, _) = QuoteContext::new(config);
1848    ///
1849    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE)
1850    ///     .await?;
1851    /// tokio::time::sleep(Duration::from_secs(5)).await;
1852    ///
1853    /// let resp = ctx.realtime_trades("700.HK", 10).await?;
1854    /// println!("{:?}", resp);
1855    /// # Ok::<_, Box<dyn std::error::Error>>(())
1856    /// # });
1857    /// ```
1858    pub async fn realtime_trades(
1859        &self,
1860        symbol: impl Into<String>,
1861        count: usize,
1862    ) -> Result<Vec<Trade>> {
1863        let (reply_tx, reply_rx) = oneshot::channel();
1864        self.0
1865            .command_tx
1866            .send(Command::GetRealtimeTrade {
1867                symbol: symbol.into(),
1868                count,
1869                reply_tx,
1870            })
1871            .map_err(|_| WsClientError::ClientClosed)?;
1872        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1873    }
1874
1875    /// Get real-time broker queue
1876    ///
1877    ///
1878    /// Get real-time broker queue of the subscribed symbols, it always returns
1879    /// the data in the local storage.
1880    ///
1881    /// # Examples
1882    ///
1883    /// ```no_run
1884    /// use std::{sync::Arc, time::Duration};
1885    ///
1886    /// use longbridge::{
1887    ///     Config,
1888    ///     oauth::OAuthBuilder,
1889    ///     quote::{QuoteContext, SubFlags},
1890    /// };
1891    ///
1892    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1893    /// let oauth = OAuthBuilder::new("your-client-id")
1894    ///     .build(|url| println!("Visit: {url}"))
1895    ///     .await?;
1896    /// let config = Arc::new(Config::from_oauth(oauth));
1897    /// let (ctx, _) = QuoteContext::new(config);
1898    ///
1899    /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER)
1900    ///     .await?;
1901    /// tokio::time::sleep(Duration::from_secs(5)).await;
1902    ///
1903    /// let resp = ctx.realtime_brokers("700.HK").await?;
1904    /// println!("{:?}", resp);
1905    /// # Ok::<_, Box<dyn std::error::Error>>(())
1906    /// # });
1907    /// ```
1908    pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
1909        let (reply_tx, reply_rx) = oneshot::channel();
1910        self.0
1911            .command_tx
1912            .send(Command::GetRealtimeBrokers {
1913                symbol: symbol.into(),
1914                reply_tx,
1915            })
1916            .map_err(|_| WsClientError::ClientClosed)?;
1917        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1918    }
1919
1920    /// Get real-time candlesticks
1921    ///
1922    /// Get real-time candlesticks of the subscribed symbols, it always returns
1923    /// the data in the local storage.
1924    ///
1925    /// # Examples
1926    ///
1927    /// ```no_run
1928    /// use std::{sync::Arc, time::Duration};
1929    ///
1930    /// use longbridge::{
1931    ///     Config,
1932    ///     oauth::OAuthBuilder,
1933    ///     quote::{Period, QuoteContext, TradeSessions},
1934    /// };
1935    ///
1936    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1937    /// let oauth = OAuthBuilder::new("your-client-id")
1938    ///     .build(|url| println!("Visit: {url}"))
1939    ///     .await?;
1940    /// let config = Arc::new(Config::from_oauth(oauth));
1941    /// let (ctx, _) = QuoteContext::new(config);
1942    ///
1943    /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
1944    ///     .await?;
1945    /// tokio::time::sleep(Duration::from_secs(5)).await;
1946    ///
1947    /// let resp = ctx
1948    ///     .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
1949    ///     .await?;
1950    /// println!("{:?}", resp);
1951    /// # Ok::<_, Box<dyn std::error::Error>>(())
1952    /// # });
1953    /// ```
1954    pub async fn realtime_candlesticks(
1955        &self,
1956        symbol: impl Into<String>,
1957        period: Period,
1958        count: usize,
1959    ) -> Result<Vec<Candlestick>> {
1960        let (reply_tx, reply_rx) = oneshot::channel();
1961        self.0
1962            .command_tx
1963            .send(Command::GetRealtimeCandlesticks {
1964                symbol: symbol.into(),
1965                period,
1966                count,
1967                reply_tx,
1968            })
1969            .map_err(|_| WsClientError::ClientClosed)?;
1970        Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1971    }
1972
1973    // ── short_positions ───────────────────────────────────────────
1974
1975    /// Get short interest data for a US or HK security.
1976    ///
1977    /// Market is inferred from the symbol suffix:
1978    /// - `.HK` → `GET /v1/quote/short-positions/hk`
1979    /// - otherwise → `GET /v1/quote/short-positions/us`
1980    ///
1981    /// `count` controls the number of records returned (1–100, default 20).
1982    pub async fn short_positions(
1983        &self,
1984        symbol: impl Into<String>,
1985        count: u32,
1986    ) -> Result<ShortPositionsResponse> {
1987        use std::time::{SystemTime, UNIX_EPOCH};
1988
1989        use crate::utils::counter::symbol_to_counter_id;
1990
1991        let sym = symbol.into();
1992        let is_hk = sym.to_uppercase().ends_with(".HK");
1993        let path = if is_hk {
1994            "/v1/quote/short-positions/hk"
1995        } else {
1996            "/v1/quote/short-positions/us"
1997        };
1998        let ts = SystemTime::now()
1999            .duration_since(UNIX_EPOCH)
2000            .map(|d| d.as_secs())
2001            .unwrap_or(0);
2002
2003        #[derive(serde::Serialize)]
2004        struct Query {
2005            counter_id: String,
2006            last_timestamp: String,
2007            count: u32,
2008        }
2009        // Response: {"counter_id":"ST/US/AAPL","data":[{...}]}
2010        let outer: serde_json::Value = self
2011            .0
2012            .http_cli
2013            .request(Method::GET, path)
2014            .query_params(Query {
2015                counter_id: symbol_to_counter_id(&sym),
2016                last_timestamp: ts.to_string(),
2017                count,
2018            })
2019            .response::<Json<serde_json::Value>>()
2020            .send()
2021            .with_subscriber(self.0.log_subscriber.clone())
2022            .await?
2023            .0;
2024        let empty = vec![];
2025        let raw = outer["data"].as_array().unwrap_or(&empty);
2026        let data = raw
2027            .iter()
2028            .map(|v| {
2029                let ts_str = v["timestamp"].as_str().unwrap_or("").to_string();
2030                ShortPositionsItem {
2031                    timestamp: unix_secs_to_rfc3339(&ts_str),
2032                    rate: v["rate"].as_str().unwrap_or("").to_string(),
2033                    close: v["close"].as_str().unwrap_or("").to_string(),
2034                    current_shares_short: v["current_shares_short"]
2035                        .as_str()
2036                        .unwrap_or("")
2037                        .to_string(),
2038                    avg_daily_share_volume: v["avg_daily_share_volume"]
2039                        .as_str()
2040                        .unwrap_or("")
2041                        .to_string(),
2042                    days_to_cover: v["days_to_cover"].as_str().unwrap_or("").to_string(),
2043                    amount: v["amount"].as_str().unwrap_or("").to_string(),
2044                    balance: v["balance"].as_str().unwrap_or("").to_string(),
2045                    cost: v["cost"].as_str().unwrap_or("").to_string(),
2046                }
2047            })
2048            .collect();
2049        Ok(ShortPositionsResponse { data })
2050    }
2051
2052    // ── option_volume ─────────────────────────────────────────────
2053
2054    /// Get real-time option call/put volume for a security.
2055    ///
2056    /// Path: `GET /v1/quote/option-volume-stats`
2057    pub async fn option_volume(&self, symbol: impl Into<String>) -> Result<OptionVolumeStats> {
2058        use crate::utils::counter::symbol_to_counter_id;
2059        #[derive(serde::Serialize)]
2060        struct Query {
2061            underlying_counter_id: String,
2062        }
2063        let resp = self
2064            .0
2065            .http_cli
2066            .request(Method::GET, "/v1/quote/option-volume-stats")
2067            .query_params(Query {
2068                underlying_counter_id: symbol_to_counter_id(&symbol.into()),
2069            })
2070            .response::<Json<OptionVolumeStats>>()
2071            .send()
2072            .with_subscriber(self.0.log_subscriber.clone())
2073            .await?;
2074        Ok(resp.0)
2075    }
2076
2077    /// Get daily historical option volume for a security.
2078    ///
2079    /// Path: `GET /v1/quote/option-volume-stats/daily`
2080    pub async fn option_volume_daily(
2081        &self,
2082        symbol: impl Into<String>,
2083        timestamp: i64,
2084        count: u32,
2085    ) -> Result<OptionVolumeDaily> {
2086        use crate::utils::counter::symbol_to_counter_id;
2087        #[derive(serde::Serialize)]
2088        struct Query {
2089            counter_id: String,
2090            timestamp: i64,
2091            line_num: u32,
2092            direction: i32,
2093        }
2094        let resp = self
2095            .0
2096            .http_cli
2097            .request(Method::GET, "/v1/quote/option-volume-stats/daily")
2098            .query_params(Query {
2099                counter_id: symbol_to_counter_id(&symbol.into()),
2100                timestamp,
2101                line_num: count,
2102                direction: 1,
2103            })
2104            .response::<Json<OptionVolumeDaily>>()
2105            .send()
2106            .with_subscriber(self.0.log_subscriber.clone())
2107            .await?;
2108        Ok(resp.0)
2109    }
2110    // ── short_trades ──────────────────────────────────────────────
2111
2112    /// Get short trade records for a HK or US security.
2113    ///
2114    /// The API endpoint is auto-detected from the symbol suffix:
2115    /// `.HK` → `GET /v1/quote/short-trades/hk`,
2116    /// otherwise → `GET /v1/quote/short-trades/us`.
2117    pub async fn short_trades(
2118        &self,
2119        symbol: impl Into<String>,
2120        count: u32,
2121    ) -> Result<ShortTradesResponse> {
2122        use std::time::{SystemTime, UNIX_EPOCH};
2123
2124        use crate::utils::counter::symbol_to_counter_id;
2125        #[derive(serde::Serialize)]
2126        struct Query {
2127            counter_id: String,
2128            last_timestamp: String,
2129            page_size: String,
2130        }
2131        let sym = symbol.into();
2132        let path = if sym.to_uppercase().ends_with(".HK") {
2133            "/v1/quote/short-trades/hk"
2134        } else {
2135            "/v1/quote/short-trades/us"
2136        };
2137        let ts = SystemTime::now()
2138            .duration_since(UNIX_EPOCH)
2139            .map(|d| d.as_secs())
2140            .unwrap_or(0);
2141        // Response: {"counter_id":"ST/HK/700","data":[{...}]}
2142        let outer: serde_json::Value = self
2143            .0
2144            .http_cli
2145            .request(Method::GET, path)
2146            .query_params(Query {
2147                counter_id: symbol_to_counter_id(&sym),
2148                last_timestamp: ts.to_string(),
2149                page_size: count.to_string(),
2150            })
2151            .response::<Json<serde_json::Value>>()
2152            .send()
2153            .with_subscriber(self.0.log_subscriber.clone())
2154            .await?
2155            .0;
2156        let empty = vec![];
2157        let raw = outer["data"].as_array().unwrap_or(&empty);
2158        let data = raw
2159            .iter()
2160            .map(|v| {
2161                let ts_str = v["timestamp"].as_str().unwrap_or("").to_string();
2162                ShortTradesItem {
2163                    timestamp: unix_secs_to_rfc3339(&ts_str),
2164                    rate: v["rate"].as_str().unwrap_or("").to_string(),
2165                    close: v["close"].as_str().unwrap_or("").to_string(),
2166                    nus_amount: v["nus_amount"].as_str().unwrap_or("").to_string(),
2167                    ny_amount: v["ny_amount"].as_str().unwrap_or("").to_string(),
2168                    total_amount: v["total_amount"].as_str().unwrap_or("").to_string(),
2169                    amount: v["amount"].as_str().unwrap_or("").to_string(),
2170                    balance: v["balance"].as_str().unwrap_or("").to_string(),
2171                }
2172            })
2173            .collect();
2174        Ok(ShortTradesResponse { data })
2175    }
2176
2177    // ── update_pinned ─────────────────────────────────────────────
2178
2179    /// Pin or unpin watchlist securities.
2180    ///
2181    /// Path: `POST /v1/watchlist/pinned`
2182    pub async fn update_pinned(&self, mode: PinnedMode, symbols: Vec<String>) -> Result<()> {
2183        #[derive(Debug, Serialize)]
2184        struct Request {
2185            mode: PinnedMode,
2186            securities: Vec<String>,
2187        }
2188
2189        self.0
2190            .http_cli
2191            .request(Method::POST, "/v1/watchlist/pinned")
2192            .body(Json(Request {
2193                mode,
2194                securities: symbols,
2195            }))
2196            .send()
2197            .with_subscriber(self.0.log_subscriber.clone())
2198            .await?;
2199
2200        Ok(())
2201    }
2202
2203    // ── symbol_to_counter_ids ─────────────────────────────────────
2204
2205    /// Batch convert symbols to counter IDs via the remote API.
2206    ///
2207    /// Returns a map of `symbol → counter_id` (e.g. `DRAM.US` →
2208    /// `ETF/US/DRAM`). Symbols the backend does not recognize are omitted
2209    /// from the result.
2210    ///
2211    /// Path: `POST /v1/quote/symbol-to-counter-ids`
2212    pub async fn symbol_to_counter_ids(
2213        &self,
2214        symbols: Vec<String>,
2215    ) -> Result<HashMap<String, String>> {
2216        #[derive(Debug, Serialize)]
2217        struct Request {
2218            ticker_regions: Vec<String>,
2219        }
2220        #[derive(Debug, Deserialize)]
2221        struct Response {
2222            #[serde(default)]
2223            list: HashMap<String, String>,
2224        }
2225
2226        let resp = self
2227            .0
2228            .http_cli
2229            .request(Method::POST, "/v1/quote/symbol-to-counter-ids")
2230            .body(Json(Request {
2231                ticker_regions: symbols,
2232            }))
2233            .response::<Json<Response>>()
2234            .send()
2235            .with_subscriber(self.0.log_subscriber.clone())
2236            .await?;
2237        Ok(resp.0.list)
2238    }
2239
2240    /// Resolve counter IDs for symbols, local-first with remote fallback.
2241    ///
2242    /// Symbols found in the embedded ETF / index / warrant directory (or in
2243    /// the local cache of previous remote resolutions) are resolved without
2244    /// network access. The remaining symbols are resolved in one batch via
2245    /// [`symbol_to_counter_ids`](Self::symbol_to_counter_ids) and the results
2246    /// are persisted to the local cache for subsequent lookups. Symbols the
2247    /// backend does not recognize fall back to the default `ST/` conversion.
2248    pub async fn resolve_counter_ids(
2249        &self,
2250        symbols: Vec<String>,
2251    ) -> Result<HashMap<String, String>> {
2252        use crate::utils::counter;
2253
2254        let mut result = HashMap::with_capacity(symbols.len());
2255        let mut unknown = Vec::new();
2256        for symbol in symbols {
2257            match counter::lookup_counter_id(&symbol) {
2258                Some(counter_id) => {
2259                    result.insert(symbol, counter_id);
2260                }
2261                None => unknown.push(symbol),
2262            }
2263        }
2264        if !unknown.is_empty() {
2265            let resolved = self.symbol_to_counter_ids(unknown.clone()).await?;
2266            counter::cache_counter_ids(resolved.values().map(String::as_str));
2267            for symbol in unknown {
2268                let counter_id = resolved
2269                    .get(&symbol)
2270                    .cloned()
2271                    .unwrap_or_else(|| counter::symbol_to_counter_id(&symbol));
2272                result.insert(symbol, counter_id);
2273            }
2274        }
2275        Ok(result)
2276    }
2277}
2278
2279fn normalize_symbol(symbol: &str) -> &str {
2280    match symbol.split_once('.') {
2281        Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
2282        _ => symbol,
2283    }
2284}