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