longbridge/quote/context.rs
1use std::{sync::Arc, time::Duration};
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use longbridge_proto::quote;
5use longbridge_wscli::WsClientError;
6use serde::{Deserialize, Serialize};
7use time::{Date, PrimitiveDateTime};
8use tokio::sync::{mpsc, oneshot};
9use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
10
11use crate::{
12 Config, Error, Language, Market, Result,
13 quote::{
14 AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine,
15 HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature,
16 MarketTradingDays, MarketTradingSession, OptionQuote, ParticipantInfo, Period, PushEvent,
17 QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup,
18 RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth,
19 SecurityListCategory, SecurityQuote, SecurityStaticInfo, StrikePriceInfo, Subscription,
20 Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup,
21 cache::{Cache, CacheWithKey},
22 cmd_code,
23 core::{Command, Core},
24 sub_flags::SubFlags,
25 types::{
26 FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, SecuritiesUpdateMode,
27 SortOrderType, WarrantSortBy, WarrantStatus,
28 },
29 utils::{format_date, parse_date},
30 },
31 serde_utils,
32};
33
34const RETRY_COUNT: usize = 3;
35const PARTICIPANT_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
36const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
37const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
38const OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
39const TRADING_SESSION_CACHE_TIMEOUT: Duration = Duration::from_secs(60 * 60 * 2);
40
41struct InnerQuoteContext {
42 language: Language,
43 http_cli: HttpClient,
44 command_tx: mpsc::UnboundedSender<Command>,
45 cache_participants: Cache<Vec<ParticipantInfo>>,
46 cache_issuers: Cache<Vec<IssuerInfo>>,
47 cache_option_chain_expiry_date_list: CacheWithKey<String, Vec<Date>>,
48 cache_option_chain_strike_info: CacheWithKey<(String, Date), Vec<StrikePriceInfo>>,
49 cache_trading_session: Cache<Vec<MarketTradingSession>>,
50 member_id: i64,
51 quote_level: String,
52 quote_package_details: Vec<QuotePackageDetail>,
53 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
54}
55
56impl Drop for InnerQuoteContext {
57 fn drop(&mut self) {
58 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
59 tracing::info!("quote context dropped");
60 });
61 }
62}
63
64/// Quote context
65#[derive(Clone)]
66pub struct QuoteContext(Arc<InnerQuoteContext>);
67
68impl QuoteContext {
69 /// Create a `QuoteContext`
70 pub async fn try_new(
71 config: Arc<Config>,
72 ) -> Result<(Self, mpsc::UnboundedReceiver<PushEvent>)> {
73 let log_subscriber = config.create_log_subscriber("quote");
74
75 dispatcher::with_default(&log_subscriber.clone().into(), || {
76 tracing::info!(
77 language = ?config.language,
78 enable_overnight = ?config.enable_overnight,
79 push_candlestick_mode = ?config.push_candlestick_mode,
80 enable_print_quote_packages = ?config.enable_print_quote_packages,
81 "creating quote context"
82 );
83 });
84
85 let language = config.language;
86 let http_cli = config.create_http_client();
87 let (command_tx, command_rx) = mpsc::unbounded_channel();
88 let (push_tx, push_rx) = mpsc::unbounded_channel();
89 let core = Core::try_new(config, command_rx, push_tx)
90 .with_subscriber(log_subscriber.clone())
91 .await?;
92 let member_id = core.member_id();
93 let quote_level = core.quote_level().to_string();
94 let quote_package_details = core.quote_package_details().to_vec();
95 tokio::spawn(core.run().with_subscriber(log_subscriber.clone()));
96
97 dispatcher::with_default(&log_subscriber.clone().into(), || {
98 tracing::info!("quote context created");
99 });
100
101 Ok((
102 QuoteContext(Arc::new(InnerQuoteContext {
103 language,
104 http_cli,
105 command_tx,
106 cache_participants: Cache::new(PARTICIPANT_INFO_CACHE_TIMEOUT),
107 cache_issuers: Cache::new(ISSUER_INFO_CACHE_TIMEOUT),
108 cache_option_chain_expiry_date_list: CacheWithKey::new(
109 OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT,
110 ),
111 cache_option_chain_strike_info: CacheWithKey::new(
112 OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT,
113 ),
114 cache_trading_session: Cache::new(TRADING_SESSION_CACHE_TIMEOUT),
115 member_id,
116 quote_level,
117 quote_package_details,
118 log_subscriber,
119 })),
120 push_rx,
121 ))
122 }
123
124 /// Returns the log subscriber
125 #[inline]
126 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
127 self.0.log_subscriber.clone()
128 }
129
130 /// Returns the member ID
131 #[inline]
132 pub fn member_id(&self) -> i64 {
133 self.0.member_id
134 }
135
136 /// Returns the quote level
137 #[inline]
138 pub fn quote_level(&self) -> &str {
139 &self.0.quote_level
140 }
141
142 /// Returns the quote package details
143 #[inline]
144 pub fn quote_package_details(&self) -> &[QuotePackageDetail] {
145 &self.0.quote_package_details
146 }
147
148 /// Send a raw request
149 async fn request_raw(&self, command_code: u8, body: Vec<u8>) -> Result<Vec<u8>> {
150 for _ in 0..RETRY_COUNT {
151 let (reply_tx, reply_rx) = oneshot::channel();
152 self.0
153 .command_tx
154 .send(Command::Request {
155 command_code,
156 body: body.clone(),
157 reply_tx,
158 })
159 .map_err(|_| WsClientError::ClientClosed)?;
160 let res = reply_rx.await.map_err(|_| WsClientError::ClientClosed)?;
161
162 match res {
163 Ok(resp) => return Ok(resp),
164 Err(Error::WsClient(WsClientError::Cancelled)) => {}
165 Err(err) => return Err(err),
166 }
167 }
168
169 Err(Error::WsClient(WsClientError::RequestTimeout))
170 }
171
172 /// Send a request `T` to get a response `R`
173 async fn request<T, R>(&self, command_code: u8, req: T) -> Result<R>
174 where
175 T: prost::Message,
176 R: prost::Message + Default,
177 {
178 let resp = self.request_raw(command_code, req.encode_to_vec()).await?;
179 Ok(R::decode(&*resp)?)
180 }
181
182 /// Send a request to get a response `R`
183 async fn request_without_body<R>(&self, command_code: u8) -> Result<R>
184 where
185 R: prost::Message + Default,
186 {
187 let resp = self.request_raw(command_code, vec![]).await?;
188 Ok(R::decode(&*resp)?)
189 }
190
191 /// Subscribe
192 ///
193 /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/subscribe>
194 ///
195 /// # Examples
196 ///
197 /// ```no_run
198 /// use std::sync::Arc;
199 ///
200 /// use longbridge::{
201 /// Config,
202 /// oauth::OAuthBuilder,
203 /// quote::{QuoteContext, SubFlags},
204 /// };
205 ///
206 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
207 /// let oauth = OAuthBuilder::new("your-client-id")
208 /// .build(|url| println!("Visit: {url}"))
209 /// .await?;
210 /// let config = Arc::new(Config::from_oauth(oauth));
211 /// let (ctx, mut receiver) = QuoteContext::try_new(config).await?;
212 ///
213 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
214 /// .await?;
215 /// while let Some(msg) = receiver.recv().await {
216 /// println!("{:?}", msg);
217 /// }
218 /// # Ok::<_, Box<dyn std::error::Error>>(())
219 /// # });
220 /// ```
221 pub async fn subscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
222 where
223 I: IntoIterator<Item = T>,
224 T: AsRef<str>,
225 {
226 let (reply_tx, reply_rx) = oneshot::channel();
227 self.0
228 .command_tx
229 .send(Command::Subscribe {
230 symbols: symbols
231 .into_iter()
232 .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
233 .collect(),
234 sub_types: sub_types.into(),
235 reply_tx,
236 })
237 .map_err(|_| WsClientError::ClientClosed)?;
238 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
239 }
240
241 /// Unsubscribe
242 ///
243 /// Reference: <https://open.longbridge.com/en/docs/quote/subscribe/unsubscribe>
244 ///
245 /// # Examples
246 ///
247 /// ```no_run
248 /// use std::sync::Arc;
249 ///
250 /// use longbridge::{
251 /// Config,
252 /// oauth::OAuthBuilder,
253 /// quote::{QuoteContext, SubFlags},
254 /// };
255 ///
256 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
257 /// let oauth = OAuthBuilder::new("your-client-id")
258 /// .build(|url| println!("Visit: {url}"))
259 /// .await?;
260 /// let config = Arc::new(Config::from_oauth(oauth));
261 /// let (ctx, _) = QuoteContext::try_new(config).await?;
262 ///
263 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
264 /// .await?;
265 /// ctx.unsubscribe(["AAPL.US"], SubFlags::QUOTE).await?;
266 /// # Ok::<_, Box<dyn std::error::Error>>(())
267 /// # });
268 /// ```
269 pub async fn unsubscribe<I, T>(&self, symbols: I, sub_types: impl Into<SubFlags>) -> Result<()>
270 where
271 I: IntoIterator<Item = T>,
272 T: AsRef<str>,
273 {
274 let (reply_tx, reply_rx) = oneshot::channel();
275 self.0
276 .command_tx
277 .send(Command::Unsubscribe {
278 symbols: symbols
279 .into_iter()
280 .map(|symbol| normalize_symbol(symbol.as_ref()).to_string())
281 .collect(),
282 sub_types: sub_types.into(),
283 reply_tx,
284 })
285 .map_err(|_| WsClientError::ClientClosed)?;
286 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
287 }
288
289 /// Subscribe security candlesticks
290 ///
291 /// # Examples
292 ///
293 /// ```no_run
294 /// use std::sync::Arc;
295 ///
296 /// use longbridge::{
297 /// Config,
298 /// oauth::OAuthBuilder,
299 /// quote::{Period, QuoteContext, TradeSessions},
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, mut receiver) = QuoteContext::try_new(config).await?;
308 ///
309 /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
310 /// .await?;
311 /// while let Some(msg) = receiver.recv().await {
312 /// println!("{:?}", msg);
313 /// }
314 /// # Ok::<_, Box<dyn std::error::Error>>(())
315 /// # });
316 /// ```
317 pub async fn subscribe_candlesticks<T>(
318 &self,
319 symbol: T,
320 period: Period,
321 trade_sessions: TradeSessions,
322 ) -> Result<Vec<Candlestick>>
323 where
324 T: AsRef<str>,
325 {
326 let (reply_tx, reply_rx) = oneshot::channel();
327 self.0
328 .command_tx
329 .send(Command::SubscribeCandlesticks {
330 symbol: normalize_symbol(symbol.as_ref()).into(),
331 period,
332 trade_sessions,
333 reply_tx,
334 })
335 .map_err(|_| WsClientError::ClientClosed)?;
336 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
337 }
338
339 /// Unsubscribe security candlesticks
340 pub async fn unsubscribe_candlesticks<T>(&self, symbol: T, period: Period) -> Result<()>
341 where
342 T: AsRef<str>,
343 {
344 let (reply_tx, reply_rx) = oneshot::channel();
345 self.0
346 .command_tx
347 .send(Command::UnsubscribeCandlesticks {
348 symbol: normalize_symbol(symbol.as_ref()).into(),
349 period,
350 reply_tx,
351 })
352 .map_err(|_| WsClientError::ClientClosed)?;
353 reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
354 }
355
356 /// Get subscription information
357 ///
358 /// # Examples
359 ///
360 /// ```no_run
361 /// use std::sync::Arc;
362 ///
363 /// use longbridge::{
364 /// Config,
365 /// oauth::OAuthBuilder,
366 /// quote::{QuoteContext, SubFlags},
367 /// };
368 ///
369 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
370 /// let oauth = OAuthBuilder::new("your-client-id")
371 /// .build(|url| println!("Visit: {url}"))
372 /// .await?;
373 /// let config = Arc::new(Config::from_oauth(oauth));
374 /// let (ctx, _) = QuoteContext::try_new(config).await?;
375 ///
376 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
377 /// .await?;
378 /// let resp = ctx.subscriptions().await?;
379 /// println!("{:?}", resp);
380 /// # Ok::<_, Box<dyn std::error::Error>>(())
381 /// # });
382 /// ```
383 pub async fn subscriptions(&self) -> Result<Vec<Subscription>> {
384 let (reply_tx, reply_rx) = oneshot::channel();
385 self.0
386 .command_tx
387 .send(Command::Subscriptions { reply_tx })
388 .map_err(|_| WsClientError::ClientClosed)?;
389 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
390 }
391
392 /// Get basic information of securities
393 ///
394 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/static>
395 ///
396 /// # Examples
397 ///
398 /// ```no_run
399 /// use std::sync::Arc;
400 ///
401 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
402 ///
403 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
404 /// let oauth = OAuthBuilder::new("your-client-id")
405 /// .build(|url| println!("Visit: {url}"))
406 /// .await?;
407 /// let config = Arc::new(Config::from_oauth(oauth));
408 /// let (ctx, _) = QuoteContext::try_new(config).await?;
409 ///
410 /// let resp = ctx
411 /// .static_info(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
412 /// .await?;
413 /// println!("{:?}", resp);
414 /// # Ok::<_, Box<dyn std::error::Error>>(())
415 /// # });
416 /// ```
417 pub async fn static_info<I, T>(&self, symbols: I) -> Result<Vec<SecurityStaticInfo>>
418 where
419 I: IntoIterator<Item = T>,
420 T: Into<String>,
421 {
422 let resp: quote::SecurityStaticInfoResponse = self
423 .request(
424 cmd_code::GET_BASIC_INFO,
425 quote::MultiSecurityRequest {
426 symbol: symbols.into_iter().map(Into::into).collect(),
427 },
428 )
429 .await?;
430 resp.secu_static_info
431 .into_iter()
432 .map(TryInto::try_into)
433 .collect()
434 }
435
436 /// Get quote of securities
437 ///
438 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/quote>
439 ///
440 /// # Examples
441 ///
442 /// ```no_run
443 /// use std::sync::Arc;
444 ///
445 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
446 ///
447 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
448 /// let oauth = OAuthBuilder::new("your-client-id")
449 /// .build(|url| println!("Visit: {url}"))
450 /// .await?;
451 /// let config = Arc::new(Config::from_oauth(oauth));
452 /// let (ctx, _) = QuoteContext::try_new(config).await?;
453 ///
454 /// let resp = ctx
455 /// .quote(["700.HK", "AAPL.US", "TSLA.US", "NFLX.US"])
456 /// .await?;
457 /// println!("{:?}", resp);
458 /// # Ok::<_, Box<dyn std::error::Error>>(())
459 /// # });
460 /// ```
461 pub async fn quote<I, T>(&self, symbols: I) -> Result<Vec<SecurityQuote>>
462 where
463 I: IntoIterator<Item = T>,
464 T: Into<String>,
465 {
466 let resp: quote::SecurityQuoteResponse = self
467 .request(
468 cmd_code::GET_REALTIME_QUOTE,
469 quote::MultiSecurityRequest {
470 symbol: symbols.into_iter().map(Into::into).collect(),
471 },
472 )
473 .await?;
474 resp.secu_quote.into_iter().map(TryInto::try_into).collect()
475 }
476
477 /// Get quote of option securities
478 ///
479 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/option-quote>
480 ///
481 /// # Examples
482 ///
483 /// ```no_run
484 /// use std::sync::Arc;
485 ///
486 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
487 ///
488 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
489 /// let oauth = OAuthBuilder::new("your-client-id")
490 /// .build(|url| println!("Visit: {url}"))
491 /// .await?;
492 /// let config = Arc::new(Config::from_oauth(oauth));
493 /// let (ctx, _) = QuoteContext::try_new(config).await?;
494 ///
495 /// let resp = ctx.option_quote(["AAPL230317P160000.US"]).await?;
496 /// println!("{:?}", resp);
497 /// # Ok::<_, Box<dyn std::error::Error>>(())
498 /// # });
499 /// ```
500 pub async fn option_quote<I, T>(&self, symbols: I) -> Result<Vec<OptionQuote>>
501 where
502 I: IntoIterator<Item = T>,
503 T: Into<String>,
504 {
505 let resp: quote::OptionQuoteResponse = self
506 .request(
507 cmd_code::GET_REALTIME_OPTION_QUOTE,
508 quote::MultiSecurityRequest {
509 symbol: symbols.into_iter().map(Into::into).collect(),
510 },
511 )
512 .await?;
513 resp.secu_quote.into_iter().map(TryInto::try_into).collect()
514 }
515
516 /// Get quote of warrant securities
517 ///
518 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/warrant-quote>
519 ///
520 /// # Examples
521 ///
522 /// ```no_run
523 /// use std::sync::Arc;
524 ///
525 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
526 ///
527 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
528 /// let oauth = OAuthBuilder::new("your-client-id")
529 /// .build(|url| println!("Visit: {url}"))
530 /// .await?;
531 /// let config = Arc::new(Config::from_oauth(oauth));
532 /// let (ctx, _) = QuoteContext::try_new(config).await?;
533 ///
534 /// let resp = ctx.warrant_quote(["21125.HK"]).await?;
535 /// println!("{:?}", resp);
536 /// # Ok::<_, Box<dyn std::error::Error>>(())
537 /// # });
538 /// ```
539 pub async fn warrant_quote<I, T>(&self, symbols: I) -> Result<Vec<WarrantQuote>>
540 where
541 I: IntoIterator<Item = T>,
542 T: Into<String>,
543 {
544 let resp: quote::WarrantQuoteResponse = self
545 .request(
546 cmd_code::GET_REALTIME_WARRANT_QUOTE,
547 quote::MultiSecurityRequest {
548 symbol: symbols.into_iter().map(Into::into).collect(),
549 },
550 )
551 .await?;
552 resp.secu_quote.into_iter().map(TryInto::try_into).collect()
553 }
554
555 /// Get security depth
556 ///
557 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/depth>
558 ///
559 /// # Examples
560 ///
561 /// ```no_run
562 /// use std::sync::Arc;
563 ///
564 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
565 ///
566 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
567 /// let oauth = OAuthBuilder::new("your-client-id")
568 /// .build(|url| println!("Visit: {url}"))
569 /// .await?;
570 /// let config = Arc::new(Config::from_oauth(oauth));
571 /// let (ctx, _) = QuoteContext::try_new(config).await?;
572 ///
573 /// let resp = ctx.depth("700.HK").await?;
574 /// println!("{:?}", resp);
575 /// # Ok::<_, Box<dyn std::error::Error>>(())
576 /// # });
577 /// ```
578 pub async fn depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
579 let resp: quote::SecurityDepthResponse = self
580 .request(
581 cmd_code::GET_SECURITY_DEPTH,
582 quote::SecurityRequest {
583 symbol: symbol.into(),
584 },
585 )
586 .await?;
587 Ok(SecurityDepth {
588 asks: resp
589 .ask
590 .into_iter()
591 .map(TryInto::try_into)
592 .collect::<Result<Vec<_>>>()?,
593 bids: resp
594 .bid
595 .into_iter()
596 .map(TryInto::try_into)
597 .collect::<Result<Vec<_>>>()?,
598 })
599 }
600
601 /// Get security brokers
602 ///
603 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/brokers>
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::try_new(config).await?;
618 ///
619 /// let resp = ctx.brokers("700.HK").await?;
620 /// println!("{:?}", resp);
621 /// # Ok::<_, Box<dyn std::error::Error>>(())
622 /// # });
623 /// ```
624 pub async fn brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
625 let resp: quote::SecurityBrokersResponse = self
626 .request(
627 cmd_code::GET_SECURITY_BROKERS,
628 quote::SecurityRequest {
629 symbol: symbol.into(),
630 },
631 )
632 .await?;
633 Ok(SecurityBrokers {
634 ask_brokers: resp.ask_brokers.into_iter().map(Into::into).collect(),
635 bid_brokers: resp.bid_brokers.into_iter().map(Into::into).collect(),
636 })
637 }
638
639 /// Get participants
640 ///
641 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/broker-ids>
642 ///
643 /// # Examples
644 ///
645 /// ```no_run
646 /// use std::sync::Arc;
647 ///
648 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
649 ///
650 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
651 /// let oauth = OAuthBuilder::new("your-client-id")
652 /// .build(|url| println!("Visit: {url}"))
653 /// .await?;
654 /// let config = Arc::new(Config::from_oauth(oauth));
655 /// let (ctx, _) = QuoteContext::try_new(config).await?;
656 ///
657 /// let resp = ctx.participants().await?;
658 /// println!("{:?}", resp);
659 /// # Ok::<_, Box<dyn std::error::Error>>(())
660 /// # });
661 /// ```
662 pub async fn participants(&self) -> Result<Vec<ParticipantInfo>> {
663 self.0
664 .cache_participants
665 .get_or_update(|| async {
666 let resp = self
667 .request_without_body::<quote::ParticipantBrokerIdsResponse>(
668 cmd_code::GET_BROKER_IDS,
669 )
670 .await?;
671
672 Ok(resp
673 .participant_broker_numbers
674 .into_iter()
675 .map(Into::into)
676 .collect())
677 })
678 .await
679 }
680
681 /// Get security trades
682 ///
683 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade>
684 ///
685 /// # Examples
686 ///
687 /// ```no_run
688 /// use std::sync::Arc;
689 ///
690 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
691 ///
692 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
693 /// let oauth = OAuthBuilder::new("your-client-id")
694 /// .build(|url| println!("Visit: {url}"))
695 /// .await?;
696 /// let config = Arc::new(Config::from_oauth(oauth));
697 /// let (ctx, _) = QuoteContext::try_new(config).await?;
698 ///
699 /// let resp = ctx.trades("700.HK", 10).await?;
700 /// println!("{:?}", resp);
701 /// # Ok::<_, Box<dyn std::error::Error>>(())
702 /// # });
703 /// ```
704 pub async fn trades(&self, symbol: impl Into<String>, count: usize) -> Result<Vec<Trade>> {
705 let resp: quote::SecurityTradeResponse = self
706 .request(
707 cmd_code::GET_SECURITY_TRADES,
708 quote::SecurityTradeRequest {
709 symbol: symbol.into(),
710 count: count as i32,
711 },
712 )
713 .await?;
714 let trades = resp
715 .trades
716 .into_iter()
717 .map(TryInto::try_into)
718 .collect::<Result<Vec<_>>>()?;
719 Ok(trades)
720 }
721
722 /// Get security intraday lines
723 ///
724 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/intraday>
725 ///
726 /// # Examples
727 ///
728 /// ```no_run
729 /// use std::sync::Arc;
730 ///
731 /// use longbridge::{
732 /// Config,
733 /// oauth::OAuthBuilder,
734 /// quote::{QuoteContext, TradeSessions},
735 /// };
736 ///
737 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
738 /// let oauth = OAuthBuilder::new("your-client-id")
739 /// .build(|url| println!("Visit: {url}"))
740 /// .await?;
741 /// let config = Arc::new(Config::from_oauth(oauth));
742 /// let (ctx, _) = QuoteContext::try_new(config).await?;
743 ///
744 /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
745 /// println!("{:?}", resp);
746 /// # Ok::<_, Box<dyn std::error::Error>>(())
747 /// # });
748 /// ```
749 pub async fn intraday(
750 &self,
751 symbol: impl Into<String>,
752 trade_sessions: TradeSessions,
753 ) -> Result<Vec<IntradayLine>> {
754 let resp: quote::SecurityIntradayResponse = self
755 .request(
756 cmd_code::GET_SECURITY_INTRADAY,
757 quote::SecurityIntradayRequest {
758 symbol: symbol.into(),
759 trade_session: trade_sessions as i32,
760 },
761 )
762 .await?;
763 let lines = resp
764 .lines
765 .into_iter()
766 .map(TryInto::try_into)
767 .collect::<Result<Vec<_>>>()?;
768 Ok(lines)
769 }
770
771 /// Get security candlesticks
772 ///
773 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/candlestick>
774 ///
775 /// # Examples
776 ///
777 /// ```no_run
778 /// use std::sync::Arc;
779 ///
780 /// use longbridge::{
781 /// Config,
782 /// oauth::OAuthBuilder,
783 /// quote::{AdjustType, Period, QuoteContext, TradeSessions},
784 /// };
785 ///
786 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
787 /// let oauth = OAuthBuilder::new("your-client-id")
788 /// .build(|url| println!("Visit: {url}"))
789 /// .await?;
790 /// let config = Arc::new(Config::from_oauth(oauth));
791 /// let (ctx, _) = QuoteContext::try_new(config).await?;
792 ///
793 /// let resp = ctx
794 /// .candlesticks(
795 /// "700.HK",
796 /// Period::Day,
797 /// 10,
798 /// AdjustType::NoAdjust,
799 /// TradeSessions::Intraday,
800 /// )
801 /// .await?;
802 /// println!("{:?}", resp);
803 /// # Ok::<_, Box<dyn std::error::Error>>(())
804 /// # });
805 /// ```
806 pub async fn candlesticks(
807 &self,
808 symbol: impl Into<String>,
809 period: Period,
810 count: usize,
811 adjust_type: AdjustType,
812 trade_sessions: TradeSessions,
813 ) -> Result<Vec<Candlestick>> {
814 let resp: quote::SecurityCandlestickResponse = self
815 .request(
816 cmd_code::GET_SECURITY_CANDLESTICKS,
817 quote::SecurityCandlestickRequest {
818 symbol: symbol.into(),
819 period: period.into(),
820 count: count as i32,
821 adjust_type: adjust_type.into(),
822 trade_session: trade_sessions as i32,
823 },
824 )
825 .await?;
826 let candlesticks = resp
827 .candlesticks
828 .into_iter()
829 .map(TryInto::try_into)
830 .collect::<Result<Vec<_>>>()?;
831 Ok(candlesticks)
832 }
833
834 /// Get security history candlesticks by offset
835 #[allow(clippy::too_many_arguments)]
836 pub async fn history_candlesticks_by_offset(
837 &self,
838 symbol: impl Into<String>,
839 period: Period,
840 adjust_type: AdjustType,
841 forward: bool,
842 time: Option<PrimitiveDateTime>,
843 count: usize,
844 trade_sessions: TradeSessions,
845 ) -> Result<Vec<Candlestick>> {
846 let resp: quote::SecurityCandlestickResponse = self
847 .request(
848 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
849 quote::SecurityHistoryCandlestickRequest {
850 symbol: symbol.into(),
851 period: period.into(),
852 adjust_type: adjust_type.into(),
853 query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
854 offset_request: Some(
855 quote::security_history_candlestick_request::OffsetQuery {
856 direction: if forward {
857 quote::Direction::Forward
858 } else {
859 quote::Direction::Backward
860 }
861 .into(),
862 date: time
863 .map(|time| {
864 format!(
865 "{:04}{:02}{:02}",
866 time.year(),
867 time.month() as u8,
868 time.day()
869 )
870 })
871 .unwrap_or_default(),
872 minute: time
873 .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
874 .unwrap_or_default(),
875 count: count as i32,
876 },
877 ),
878 date_request: None,
879 trade_session: trade_sessions as i32,
880 },
881 )
882 .await?;
883 let candlesticks = resp
884 .candlesticks
885 .into_iter()
886 .map(TryInto::try_into)
887 .collect::<Result<Vec<_>>>()?;
888 Ok(candlesticks)
889 }
890
891 /// Get security history candlesticks by date
892 pub async fn history_candlesticks_by_date(
893 &self,
894 symbol: impl Into<String>,
895 period: Period,
896 adjust_type: AdjustType,
897 start: Option<Date>,
898 end: Option<Date>,
899 trade_sessions: TradeSessions,
900 ) -> Result<Vec<Candlestick>> {
901 let resp: quote::SecurityCandlestickResponse = self
902 .request(
903 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
904 quote::SecurityHistoryCandlestickRequest {
905 symbol: symbol.into(),
906 period: period.into(),
907 adjust_type: adjust_type.into(),
908 query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
909 offset_request: None,
910 date_request: Some(quote::security_history_candlestick_request::DateQuery {
911 start_date: start
912 .map(|date| {
913 format!(
914 "{:04}{:02}{:02}",
915 date.year(),
916 date.month() as u8,
917 date.day()
918 )
919 })
920 .unwrap_or_default(),
921 end_date: end
922 .map(|date| {
923 format!(
924 "{:04}{:02}{:02}",
925 date.year(),
926 date.month() as u8,
927 date.day()
928 )
929 })
930 .unwrap_or_default(),
931 }),
932 trade_session: trade_sessions as i32,
933 },
934 )
935 .await?;
936 let candlesticks = resp
937 .candlesticks
938 .into_iter()
939 .map(TryInto::try_into)
940 .collect::<Result<Vec<_>>>()?;
941 Ok(candlesticks)
942 }
943
944 /// Get option chain expiry date list
945 ///
946 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date>
947 ///
948 /// # Examples
949 ///
950 /// ```no_run
951 /// use std::sync::Arc;
952 ///
953 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
954 ///
955 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
956 /// let oauth = OAuthBuilder::new("your-client-id")
957 /// .build(|url| println!("Visit: {url}"))
958 /// .await?;
959 /// let config = Arc::new(Config::from_oauth(oauth));
960 /// let (ctx, _) = QuoteContext::try_new(config).await?;
961 ///
962 /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
963 /// println!("{:?}", resp);
964 /// # Ok::<_, Box<dyn std::error::Error>>(())
965 /// # });
966 /// ```
967 pub async fn option_chain_expiry_date_list(
968 &self,
969 symbol: impl Into<String>,
970 ) -> Result<Vec<Date>> {
971 self.0
972 .cache_option_chain_expiry_date_list
973 .get_or_update(symbol.into(), |symbol| async {
974 let resp: quote::OptionChainDateListResponse = self
975 .request(
976 cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
977 quote::SecurityRequest { symbol },
978 )
979 .await?;
980 resp.expiry_date
981 .iter()
982 .map(|value| {
983 parse_date(value).map_err(|err| Error::parse_field_error("date", err))
984 })
985 .collect::<Result<Vec<_>>>()
986 })
987 .await
988 }
989
990 /// Get option chain info by date
991 ///
992 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/optionchain-date-strike>
993 ///
994 /// # Examples
995 ///
996 /// ```no_run
997 /// use std::sync::Arc;
998 ///
999 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1000 /// use time::macros::date;
1001 ///
1002 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1003 /// let oauth = OAuthBuilder::new("your-client-id")
1004 /// .build(|url| println!("Visit: {url}"))
1005 /// .await?;
1006 /// let config = Arc::new(Config::from_oauth(oauth));
1007 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1008 ///
1009 /// let resp = ctx
1010 /// .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))
1011 /// .await?;
1012 /// println!("{:?}", resp);
1013 /// # Ok::<_, Box<dyn std::error::Error>>(())
1014 /// # });
1015 /// ```
1016 pub async fn option_chain_info_by_date(
1017 &self,
1018 symbol: impl Into<String>,
1019 expiry_date: Date,
1020 ) -> Result<Vec<StrikePriceInfo>> {
1021 self.0
1022 .cache_option_chain_strike_info
1023 .get_or_update(
1024 (symbol.into(), expiry_date),
1025 |(symbol, expiry_date)| async move {
1026 let resp: quote::OptionChainDateStrikeInfoResponse = self
1027 .request(
1028 cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE,
1029 quote::OptionChainDateStrikeInfoRequest {
1030 symbol,
1031 expiry_date: format_date(expiry_date),
1032 },
1033 )
1034 .await?;
1035 resp.strike_price_info
1036 .into_iter()
1037 .map(TryInto::try_into)
1038 .collect::<Result<Vec<_>>>()
1039 },
1040 )
1041 .await
1042 }
1043
1044 /// Get warrant issuers
1045 ///
1046 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/issuer>
1047 ///
1048 /// # Examples
1049 ///
1050 /// ```no_run
1051 /// use std::sync::Arc;
1052 ///
1053 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1054 ///
1055 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1056 /// let oauth = OAuthBuilder::new("your-client-id")
1057 /// .build(|url| println!("Visit: {url}"))
1058 /// .await?;
1059 /// let config = Arc::new(Config::from_oauth(oauth));
1060 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1061 ///
1062 /// let resp = ctx.warrant_issuers().await?;
1063 /// println!("{:?}", resp);
1064 /// # Ok::<_, Box<dyn std::error::Error>>(())
1065 /// # });
1066 /// ```
1067 pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1068 self.0
1069 .cache_issuers
1070 .get_or_update(|| async {
1071 let resp = self
1072 .request_without_body::<quote::IssuerInfoResponse>(
1073 cmd_code::GET_WARRANT_ISSUER_IDS,
1074 )
1075 .await?;
1076 Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1077 })
1078 .await
1079 }
1080
1081 /// Query warrant list
1082 #[allow(clippy::too_many_arguments)]
1083 pub async fn warrant_list(
1084 &self,
1085 symbol: impl Into<String>,
1086 sort_by: WarrantSortBy,
1087 sort_order: SortOrderType,
1088 warrant_type: Option<&[WarrantType]>,
1089 issuer: Option<&[i32]>,
1090 expiry_date: Option<&[FilterWarrantExpiryDate]>,
1091 price_type: Option<&[FilterWarrantInOutBoundsType]>,
1092 status: Option<&[WarrantStatus]>,
1093 ) -> Result<Vec<WarrantInfo>> {
1094 let resp = self
1095 .request::<_, quote::WarrantFilterListResponse>(
1096 cmd_code::GET_FILTERED_WARRANT,
1097 quote::WarrantFilterListRequest {
1098 symbol: symbol.into(),
1099 filter_config: Some(quote::FilterConfig {
1100 sort_by: sort_by.into(),
1101 sort_order: sort_order.into(),
1102 sort_offset: 0,
1103 sort_count: 0,
1104 r#type: warrant_type
1105 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1106 .unwrap_or_default(),
1107 issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1108 expiry_date: expiry_date
1109 .map(|e| e.iter().map(|e| (*e).into()).collect())
1110 .unwrap_or_default(),
1111 price_type: price_type
1112 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1113 .unwrap_or_default(),
1114 status: status
1115 .map(|status| status.iter().map(|status| (*status).into()).collect())
1116 .unwrap_or_default(),
1117 }),
1118 language: self.0.language.into(),
1119 },
1120 )
1121 .await?;
1122 resp.warrant_list
1123 .into_iter()
1124 .map(TryInto::try_into)
1125 .collect::<Result<Vec<_>>>()
1126 }
1127
1128 /// Get trading session of the day
1129 ///
1130 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-session>
1131 ///
1132 /// # Examples
1133 ///
1134 /// ```no_run
1135 /// use std::sync::Arc;
1136 ///
1137 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1138 ///
1139 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1140 /// let oauth = OAuthBuilder::new("your-client-id")
1141 /// .build(|url| println!("Visit: {url}"))
1142 /// .await?;
1143 /// let config = Arc::new(Config::from_oauth(oauth));
1144 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1145 ///
1146 /// let resp = ctx.trading_session().await?;
1147 /// println!("{:?}", resp);
1148 /// # Ok::<_, Box<dyn std::error::Error>>(())
1149 /// # });
1150 /// ```
1151 pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1152 self.0
1153 .cache_trading_session
1154 .get_or_update(|| async {
1155 let resp = self
1156 .request_without_body::<quote::MarketTradePeriodResponse>(
1157 cmd_code::GET_TRADING_SESSION,
1158 )
1159 .await?;
1160 resp.market_trade_session
1161 .into_iter()
1162 .map(TryInto::try_into)
1163 .collect::<Result<Vec<_>>>()
1164 })
1165 .await
1166 }
1167
1168 /// Get market trading days
1169 ///
1170 /// The interval must be less than one month, and only the most recent year
1171 /// is supported.
1172 ///
1173 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/trade-day>
1174 ///
1175 /// # Examples
1176 ///
1177 /// ```no_run
1178 /// use std::sync::Arc;
1179 ///
1180 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1181 /// use time::macros::date;
1182 ///
1183 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1184 /// let oauth = OAuthBuilder::new("your-client-id")
1185 /// .build(|url| println!("Visit: {url}"))
1186 /// .await?;
1187 /// let config = Arc::new(Config::from_oauth(oauth));
1188 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1189 ///
1190 /// let resp = ctx
1191 /// .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1192 /// .await?;
1193 /// println!("{:?}", resp);
1194 /// # Ok::<_, Box<dyn std::error::Error>>(())
1195 /// # });
1196 /// ```
1197 pub async fn trading_days(
1198 &self,
1199 market: Market,
1200 begin: Date,
1201 end: Date,
1202 ) -> Result<MarketTradingDays> {
1203 let resp = self
1204 .request::<_, quote::MarketTradeDayResponse>(
1205 cmd_code::GET_TRADING_DAYS,
1206 quote::MarketTradeDayRequest {
1207 market: market.to_string(),
1208 beg_day: format_date(begin),
1209 end_day: format_date(end),
1210 },
1211 )
1212 .await?;
1213 let trading_days = resp
1214 .trade_day
1215 .iter()
1216 .map(|value| {
1217 parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1218 })
1219 .collect::<Result<Vec<_>>>()?;
1220 let half_trading_days = resp
1221 .half_trade_day
1222 .iter()
1223 .map(|value| {
1224 parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1225 })
1226 .collect::<Result<Vec<_>>>()?;
1227 Ok(MarketTradingDays {
1228 trading_days,
1229 half_trading_days,
1230 })
1231 }
1232
1233 /// Get capital flow intraday
1234 ///
1235 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-flow-intraday>
1236 ///
1237 /// # Examples
1238 ///
1239 /// ```no_run
1240 /// use std::sync::Arc;
1241 ///
1242 /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1243 ///
1244 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1245 /// let oauth = OAuthBuilder::new("your-client-id")
1246 /// .build(|url| println!("Visit: {url}"))
1247 /// .await?;
1248 /// let config = Arc::new(Config::from_oauth(oauth));
1249 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1250 ///
1251 /// let resp = ctx.capital_flow("700.HK").await?;
1252 /// println!("{:?}", resp);
1253 /// # Ok::<_, Box<dyn std::error::Error>>(())
1254 /// # });
1255 pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1256 self.request::<_, quote::CapitalFlowIntradayResponse>(
1257 cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1258 quote::CapitalFlowIntradayRequest {
1259 symbol: symbol.into(),
1260 },
1261 )
1262 .await?
1263 .capital_flow_lines
1264 .into_iter()
1265 .map(TryInto::try_into)
1266 .collect()
1267 }
1268
1269 /// Get capital distribution
1270 ///
1271 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/capital-distribution>
1272 ///
1273 /// # Examples
1274 ///
1275 /// ```no_run
1276 /// use std::sync::Arc;
1277 ///
1278 /// use longbridge::{oauth::OAuthBuilder, quote::QuoteContext, Config};
1279 ///
1280 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1281 /// let oauth = OAuthBuilder::new("your-client-id")
1282 /// .build(|url| println!("Visit: {url}"))
1283 /// .await?;
1284 /// let config = Arc::new(Config::from_oauth(oauth));
1285 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1286 ///
1287 /// let resp = ctx.capital_distribution("700.HK").await?;
1288 /// println!("{:?}", resp);
1289 /// # Ok::<_, Box<dyn std::error::Error>>(())
1290 /// # });
1291 pub async fn capital_distribution(
1292 &self,
1293 symbol: impl Into<String>,
1294 ) -> Result<CapitalDistributionResponse> {
1295 self.request::<_, quote::CapitalDistributionResponse>(
1296 cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1297 quote::SecurityRequest {
1298 symbol: symbol.into(),
1299 },
1300 )
1301 .await?
1302 .try_into()
1303 }
1304
1305 /// Get calc indexes
1306 pub async fn calc_indexes<I, T, J>(
1307 &self,
1308 symbols: I,
1309 indexes: J,
1310 ) -> Result<Vec<SecurityCalcIndex>>
1311 where
1312 I: IntoIterator<Item = T>,
1313 T: Into<String>,
1314 J: IntoIterator<Item = CalcIndex>,
1315 {
1316 let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1317 let resp: quote::SecurityCalcQuoteResponse = self
1318 .request(
1319 cmd_code::GET_CALC_INDEXES,
1320 quote::SecurityCalcQuoteRequest {
1321 symbols: symbols.into_iter().map(Into::into).collect(),
1322 calc_index: indexes
1323 .iter()
1324 .map(|i| quote::CalcIndex::from(*i).into())
1325 .collect(),
1326 },
1327 )
1328 .await?;
1329
1330 Ok(resp
1331 .security_calc_index
1332 .into_iter()
1333 .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1334 .collect())
1335 }
1336
1337 /// Get watchlist
1338 ///
1339 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_groups>
1340 ///
1341 /// # Examples
1342 ///
1343 /// ```no_run
1344 /// use std::sync::Arc;
1345 ///
1346 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1347 ///
1348 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1349 /// let oauth = OAuthBuilder::new("your-client-id")
1350 /// .build(|url| println!("Visit: {url}"))
1351 /// .await?;
1352 /// let config = Arc::new(Config::from_oauth(oauth));
1353 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1354 ///
1355 /// let resp = ctx.watchlist().await?;
1356 /// println!("{:?}", resp);
1357 /// # Ok::<_, Box<dyn std::error::Error>>(())
1358 /// # });
1359 /// ```
1360 pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1361 #[derive(Debug, Deserialize)]
1362 struct Response {
1363 groups: Vec<WatchlistGroup>,
1364 }
1365
1366 let resp = self
1367 .0
1368 .http_cli
1369 .request(Method::GET, "/v1/watchlist/groups")
1370 .response::<Json<Response>>()
1371 .send()
1372 .with_subscriber(self.0.log_subscriber.clone())
1373 .await?;
1374 Ok(resp.0.groups)
1375 }
1376
1377 /// Create watchlist group
1378 ///
1379 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_create_group>
1380 ///
1381 /// # Examples
1382 ///
1383 /// ```no_run
1384 /// use std::sync::Arc;
1385 ///
1386 /// use longbridge::{
1387 /// Config,
1388 /// oauth::OAuthBuilder,
1389 /// quote::{QuoteContext, RequestCreateWatchlistGroup},
1390 /// };
1391 ///
1392 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1393 /// let oauth = OAuthBuilder::new("your-client-id")
1394 /// .build(|url| println!("Visit: {url}"))
1395 /// .await?;
1396 /// let config = Arc::new(Config::from_oauth(oauth));
1397 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1398 ///
1399 /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1400 /// let group_id = ctx.create_watchlist_group(req).await?;
1401 /// println!("{}", group_id);
1402 /// # Ok::<_, Box<dyn std::error::Error>>(())
1403 /// # });
1404 /// ```
1405 pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1406 #[derive(Debug, Serialize)]
1407 struct RequestCreate {
1408 name: String,
1409 #[serde(skip_serializing_if = "Option::is_none")]
1410 securities: Option<Vec<String>>,
1411 }
1412
1413 #[derive(Debug, Deserialize)]
1414 struct Response {
1415 #[serde(with = "serde_utils::int64_str")]
1416 id: i64,
1417 }
1418
1419 let Json(Response { id }) = self
1420 .0
1421 .http_cli
1422 .request(Method::POST, "/v1/watchlist/groups")
1423 .body(Json(RequestCreate {
1424 name: req.name,
1425 securities: req.securities,
1426 }))
1427 .response::<Json<Response>>()
1428 .send()
1429 .with_subscriber(self.0.log_subscriber.clone())
1430 .await?;
1431
1432 Ok(id)
1433 }
1434
1435 /// Delete watchlist group
1436 ///
1437 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_delete_group>
1438 ///
1439 /// # Examples
1440 ///
1441 /// ```no_run
1442 /// use std::sync::Arc;
1443 ///
1444 /// use longbridge::{Config, oauth::OAuthBuilder, quote::QuoteContext};
1445 ///
1446 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1447 /// let oauth = OAuthBuilder::new("your-client-id")
1448 /// .build(|url| println!("Visit: {url}"))
1449 /// .await?;
1450 /// let config = Arc::new(Config::from_oauth(oauth));
1451 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1452 ///
1453 /// ctx.delete_watchlist_group(10086, true).await?;
1454 /// # Ok::<_, Box<dyn std::error::Error>>(())
1455 /// # });
1456 /// ```
1457 pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1458 #[derive(Debug, Serialize)]
1459 struct Request {
1460 id: i64,
1461 purge: bool,
1462 }
1463
1464 Ok(self
1465 .0
1466 .http_cli
1467 .request(Method::DELETE, "/v1/watchlist/groups")
1468 .query_params(Request { id, purge })
1469 .send()
1470 .with_subscriber(self.0.log_subscriber.clone())
1471 .await?)
1472 }
1473
1474 /// Update watchlist group
1475 ///
1476 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group>
1477 /// Reference: <https://open.longbridge.com/en/docs/quote/individual/watchlist_update_group_securities>
1478 ///
1479 /// # Examples
1480 ///
1481 /// ```no_run
1482 /// use std::sync::Arc;
1483 ///
1484 /// use longbridge::{
1485 /// Config,
1486 /// oauth::OAuthBuilder,
1487 /// quote::{QuoteContext, RequestUpdateWatchlistGroup},
1488 /// };
1489 ///
1490 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1491 /// let oauth = OAuthBuilder::new("your-client-id")
1492 /// .build(|url| println!("Visit: {url}"))
1493 /// .await?;
1494 /// let config = Arc::new(Config::from_oauth(oauth));
1495 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1496 /// let req = RequestUpdateWatchlistGroup::new(10086)
1497 /// .name("Watchlist2")
1498 /// .securities(["700.HK", "BABA.US"]);
1499 /// ctx.update_watchlist_group(req).await?;
1500 /// # Ok::<_, Box<dyn std::error::Error>>(())
1501 /// # });
1502 /// ```
1503 pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1504 #[derive(Debug, Serialize)]
1505 struct RequestUpdate {
1506 id: i64,
1507 #[serde(skip_serializing_if = "Option::is_none")]
1508 name: Option<String>,
1509 #[serde(skip_serializing_if = "Option::is_none")]
1510 securities: Option<Vec<String>>,
1511 #[serde(skip_serializing_if = "Option::is_none")]
1512 mode: Option<SecuritiesUpdateMode>,
1513 }
1514
1515 self.0
1516 .http_cli
1517 .request(Method::PUT, "/v1/watchlist/groups")
1518 .body(Json(RequestUpdate {
1519 id: req.id,
1520 name: req.name,
1521 mode: req.securities.is_some().then_some(req.mode),
1522 securities: req.securities,
1523 }))
1524 .send()
1525 .with_subscriber(self.0.log_subscriber.clone())
1526 .await?;
1527
1528 Ok(())
1529 }
1530
1531 /// Get security list
1532 pub async fn security_list(
1533 &self,
1534 market: Market,
1535 category: impl Into<Option<SecurityListCategory>>,
1536 ) -> Result<Vec<Security>> {
1537 #[derive(Debug, Serialize)]
1538 struct Request {
1539 market: Market,
1540 #[serde(skip_serializing_if = "Option::is_none")]
1541 category: Option<SecurityListCategory>,
1542 }
1543
1544 #[derive(Debug, Deserialize)]
1545 struct Response {
1546 list: Vec<Security>,
1547 }
1548
1549 Ok(self
1550 .0
1551 .http_cli
1552 .request(Method::GET, "/v1/quote/get_security_list")
1553 .query_params(Request {
1554 market,
1555 category: category.into(),
1556 })
1557 .response::<Json<Response>>()
1558 .send()
1559 .with_subscriber(self.0.log_subscriber.clone())
1560 .await?
1561 .0
1562 .list)
1563 }
1564
1565 /// Get current market temperature
1566 ///
1567 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/market_temperature>
1568 ///
1569 /// # Examples
1570 ///
1571 /// ```no_run
1572 /// use std::sync::Arc;
1573 ///
1574 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1575 ///
1576 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1577 /// let oauth = OAuthBuilder::new("your-client-id")
1578 /// .build(|url| println!("Visit: {url}"))
1579 /// .await?;
1580 /// let config = Arc::new(Config::from_oauth(oauth));
1581 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1582 ///
1583 /// let resp = ctx.market_temperature(Market::HK).await?;
1584 /// println!("{:?}", resp);
1585 /// # Ok::<_, Box<dyn std::error::Error>>(())
1586 /// # });
1587 /// ```
1588 pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1589 #[derive(Debug, Serialize)]
1590 struct Request {
1591 market: Market,
1592 }
1593
1594 Ok(self
1595 .0
1596 .http_cli
1597 .request(Method::GET, "/v1/quote/market_temperature")
1598 .query_params(Request { market })
1599 .response::<Json<MarketTemperature>>()
1600 .send()
1601 .with_subscriber(self.0.log_subscriber.clone())
1602 .await?
1603 .0)
1604 }
1605
1606 /// Get historical market temperature
1607 ///
1608 /// Reference: <https://open.longbridge.com/en/docs/quote/pull/history_market_temperature>
1609 ///
1610 /// # Examples
1611 ///
1612 /// ```no_run
1613 /// use std::sync::Arc;
1614 ///
1615 /// use longbridge::{Config, Market, oauth::OAuthBuilder, quote::QuoteContext};
1616 /// use time::macros::date;
1617 ///
1618 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1619 /// let oauth = OAuthBuilder::new("your-client-id")
1620 /// .build(|url| println!("Visit: {url}"))
1621 /// .await?;
1622 /// let config = Arc::new(Config::from_oauth(oauth));
1623 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1624 ///
1625 /// let resp = ctx
1626 /// .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1627 /// .await?;
1628 /// println!("{:?}", resp);
1629 /// # Ok::<_, Box<dyn std::error::Error>>(())
1630 /// # });
1631 /// ```
1632 pub async fn history_market_temperature(
1633 &self,
1634 market: Market,
1635 start_date: Date,
1636 end_date: Date,
1637 ) -> Result<HistoryMarketTemperatureResponse> {
1638 #[derive(Debug, Serialize)]
1639 struct Request {
1640 market: Market,
1641 start_date: String,
1642 end_date: String,
1643 }
1644
1645 Ok(self
1646 .0
1647 .http_cli
1648 .request(Method::GET, "/v1/quote/history_market_temperature")
1649 .query_params(Request {
1650 market,
1651 start_date: format_date(start_date),
1652 end_date: format_date(end_date),
1653 })
1654 .response::<Json<HistoryMarketTemperatureResponse>>()
1655 .send()
1656 .with_subscriber(self.0.log_subscriber.clone())
1657 .await?
1658 .0)
1659 }
1660
1661 /// Get real-time quotes
1662 ///
1663 /// Get real-time quotes of the subscribed symbols, it always returns the
1664 /// data in the local storage.
1665 ///
1666 /// # Examples
1667 ///
1668 /// ```no_run
1669 /// use std::{sync::Arc, time::Duration};
1670 ///
1671 /// use longbridge::{
1672 /// Config,
1673 /// oauth::OAuthBuilder,
1674 /// quote::{QuoteContext, SubFlags},
1675 /// };
1676 ///
1677 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1678 /// let oauth = OAuthBuilder::new("your-client-id")
1679 /// .build(|url| println!("Visit: {url}"))
1680 /// .await?;
1681 /// let config = Arc::new(Config::from_oauth(oauth));
1682 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1683 ///
1684 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE)
1685 /// .await?;
1686 /// tokio::time::sleep(Duration::from_secs(5)).await;
1687 ///
1688 /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
1689 /// println!("{:?}", resp);
1690 /// # Ok::<_, Box<dyn std::error::Error>>(())
1691 /// # });
1692 /// ```
1693 pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
1694 where
1695 I: IntoIterator<Item = T>,
1696 T: Into<String>,
1697 {
1698 let (reply_tx, reply_rx) = oneshot::channel();
1699 self.0
1700 .command_tx
1701 .send(Command::GetRealtimeQuote {
1702 symbols: symbols.into_iter().map(Into::into).collect(),
1703 reply_tx,
1704 })
1705 .map_err(|_| WsClientError::ClientClosed)?;
1706 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1707 }
1708
1709 /// Get real-time depth
1710 ///
1711 /// Get real-time depth of the subscribed symbols, it always returns the
1712 /// data in the local storage.
1713 ///
1714 /// # Examples
1715 ///
1716 /// ```no_run
1717 /// use std::{sync::Arc, time::Duration};
1718 ///
1719 /// use longbridge::{
1720 /// Config,
1721 /// oauth::OAuthBuilder,
1722 /// quote::{QuoteContext, SubFlags},
1723 /// };
1724 ///
1725 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1726 /// let oauth = OAuthBuilder::new("your-client-id")
1727 /// .build(|url| println!("Visit: {url}"))
1728 /// .await?;
1729 /// let config = Arc::new(Config::from_oauth(oauth));
1730 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1731 ///
1732 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH)
1733 /// .await?;
1734 /// tokio::time::sleep(Duration::from_secs(5)).await;
1735 ///
1736 /// let resp = ctx.realtime_depth("700.HK").await?;
1737 /// println!("{:?}", resp);
1738 /// # Ok::<_, Box<dyn std::error::Error>>(())
1739 /// # });
1740 /// ```
1741 pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
1742 let (reply_tx, reply_rx) = oneshot::channel();
1743 self.0
1744 .command_tx
1745 .send(Command::GetRealtimeDepth {
1746 symbol: symbol.into(),
1747 reply_tx,
1748 })
1749 .map_err(|_| WsClientError::ClientClosed)?;
1750 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1751 }
1752
1753 /// Get real-time trades
1754 ///
1755 /// Get real-time trades of the subscribed symbols, it always returns the
1756 /// data in the local storage.
1757 ///
1758 /// # Examples
1759 ///
1760 /// ```no_run
1761 /// use std::{sync::Arc, time::Duration};
1762 ///
1763 /// use longbridge::{
1764 /// Config,
1765 /// oauth::OAuthBuilder,
1766 /// quote::{QuoteContext, SubFlags},
1767 /// };
1768 ///
1769 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1770 /// let oauth = OAuthBuilder::new("your-client-id")
1771 /// .build(|url| println!("Visit: {url}"))
1772 /// .await?;
1773 /// let config = Arc::new(Config::from_oauth(oauth));
1774 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1775 ///
1776 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE)
1777 /// .await?;
1778 /// tokio::time::sleep(Duration::from_secs(5)).await;
1779 ///
1780 /// let resp = ctx.realtime_trades("700.HK", 10).await?;
1781 /// println!("{:?}", resp);
1782 /// # Ok::<_, Box<dyn std::error::Error>>(())
1783 /// # });
1784 /// ```
1785 pub async fn realtime_trades(
1786 &self,
1787 symbol: impl Into<String>,
1788 count: usize,
1789 ) -> Result<Vec<Trade>> {
1790 let (reply_tx, reply_rx) = oneshot::channel();
1791 self.0
1792 .command_tx
1793 .send(Command::GetRealtimeTrade {
1794 symbol: symbol.into(),
1795 count,
1796 reply_tx,
1797 })
1798 .map_err(|_| WsClientError::ClientClosed)?;
1799 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1800 }
1801
1802 /// Get real-time broker queue
1803 ///
1804 ///
1805 /// Get real-time broker queue of the subscribed symbols, it always returns
1806 /// the data in the local storage.
1807 ///
1808 /// # Examples
1809 ///
1810 /// ```no_run
1811 /// use std::{sync::Arc, time::Duration};
1812 ///
1813 /// use longbridge::{
1814 /// Config,
1815 /// oauth::OAuthBuilder,
1816 /// quote::{QuoteContext, SubFlags},
1817 /// };
1818 ///
1819 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1820 /// let oauth = OAuthBuilder::new("your-client-id")
1821 /// .build(|url| println!("Visit: {url}"))
1822 /// .await?;
1823 /// let config = Arc::new(Config::from_oauth(oauth));
1824 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1825 ///
1826 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER)
1827 /// .await?;
1828 /// tokio::time::sleep(Duration::from_secs(5)).await;
1829 ///
1830 /// let resp = ctx.realtime_brokers("700.HK").await?;
1831 /// println!("{:?}", resp);
1832 /// # Ok::<_, Box<dyn std::error::Error>>(())
1833 /// # });
1834 /// ```
1835 pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
1836 let (reply_tx, reply_rx) = oneshot::channel();
1837 self.0
1838 .command_tx
1839 .send(Command::GetRealtimeBrokers {
1840 symbol: symbol.into(),
1841 reply_tx,
1842 })
1843 .map_err(|_| WsClientError::ClientClosed)?;
1844 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1845 }
1846
1847 /// Get real-time candlesticks
1848 ///
1849 /// Get real-time candlesticks of the subscribed symbols, it always returns
1850 /// the data in the local storage.
1851 ///
1852 /// # Examples
1853 ///
1854 /// ```no_run
1855 /// use std::{sync::Arc, time::Duration};
1856 ///
1857 /// use longbridge::{
1858 /// Config,
1859 /// oauth::OAuthBuilder,
1860 /// quote::{Period, QuoteContext, TradeSessions},
1861 /// };
1862 ///
1863 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1864 /// let oauth = OAuthBuilder::new("your-client-id")
1865 /// .build(|url| println!("Visit: {url}"))
1866 /// .await?;
1867 /// let config = Arc::new(Config::from_oauth(oauth));
1868 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1869 ///
1870 /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
1871 /// .await?;
1872 /// tokio::time::sleep(Duration::from_secs(5)).await;
1873 ///
1874 /// let resp = ctx
1875 /// .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
1876 /// .await?;
1877 /// println!("{:?}", resp);
1878 /// # Ok::<_, Box<dyn std::error::Error>>(())
1879 /// # });
1880 /// ```
1881 pub async fn realtime_candlesticks(
1882 &self,
1883 symbol: impl Into<String>,
1884 period: Period,
1885 count: usize,
1886 ) -> Result<Vec<Candlestick>> {
1887 let (reply_tx, reply_rx) = oneshot::channel();
1888 self.0
1889 .command_tx
1890 .send(Command::GetRealtimeCandlesticks {
1891 symbol: symbol.into(),
1892 period,
1893 count,
1894 reply_tx,
1895 })
1896 .map_err(|_| WsClientError::ClientClosed)?;
1897 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1898 }
1899}
1900
1901fn normalize_symbol(symbol: &str) -> &str {
1902 match symbol.split_once('.') {
1903 Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
1904 _ => symbol,
1905 }
1906}