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}