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