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}