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