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::{
698 /// Config,
699 /// quote::{QuoteContext, TradeSessions},
700 /// };
701 ///
702 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
703 /// let config = Arc::new(Config::from_env()?);
704 /// let (ctx, _) = QuoteContext::try_new(config).await?;
705 ///
706 /// let resp = ctx.intraday("700.HK", TradeSessions::Intraday).await?;
707 /// println!("{:?}", resp);
708 /// # Ok::<_, Box<dyn std::error::Error>>(())
709 /// # });
710 /// ```
711 pub async fn intraday(
712 &self,
713 symbol: impl Into<String>,
714 trade_sessions: TradeSessions,
715 ) -> Result<Vec<IntradayLine>> {
716 let resp: quote::SecurityIntradayResponse = self
717 .request(
718 cmd_code::GET_SECURITY_INTRADAY,
719 quote::SecurityIntradayRequest {
720 symbol: symbol.into(),
721 trade_session: trade_sessions as i32,
722 },
723 )
724 .await?;
725 let lines = resp
726 .lines
727 .into_iter()
728 .map(TryInto::try_into)
729 .collect::<Result<Vec<_>>>()?;
730 Ok(lines)
731 }
732
733 /// Get security candlesticks
734 ///
735 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/candlestick>
736 ///
737 /// # Examples
738 ///
739 /// ```no_run
740 /// use std::sync::Arc;
741 ///
742 /// use longport::{
743 /// Config,
744 /// quote::{AdjustType, Period, QuoteContext, TradeSessions},
745 /// };
746 ///
747 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
748 /// let config = Arc::new(Config::from_env()?);
749 /// let (ctx, _) = QuoteContext::try_new(config).await?;
750 ///
751 /// let resp = ctx
752 /// .candlesticks(
753 /// "700.HK",
754 /// Period::Day,
755 /// 10,
756 /// AdjustType::NoAdjust,
757 /// TradeSessions::Intraday,
758 /// )
759 /// .await?;
760 /// println!("{:?}", resp);
761 /// # Ok::<_, Box<dyn std::error::Error>>(())
762 /// # });
763 /// ```
764 pub async fn candlesticks(
765 &self,
766 symbol: impl Into<String>,
767 period: Period,
768 count: usize,
769 adjust_type: AdjustType,
770 trade_sessions: TradeSessions,
771 ) -> Result<Vec<Candlestick>> {
772 let resp: quote::SecurityCandlestickResponse = self
773 .request(
774 cmd_code::GET_SECURITY_CANDLESTICKS,
775 quote::SecurityCandlestickRequest {
776 symbol: symbol.into(),
777 period: period.into(),
778 count: count as i32,
779 adjust_type: adjust_type.into(),
780 trade_session: trade_sessions as i32,
781 },
782 )
783 .await?;
784 let candlesticks = resp
785 .candlesticks
786 .into_iter()
787 .map(TryInto::try_into)
788 .collect::<Result<Vec<_>>>()?;
789 Ok(candlesticks)
790 }
791
792 /// Get security history candlesticks by offset
793 #[allow(clippy::too_many_arguments)]
794 pub async fn history_candlesticks_by_offset(
795 &self,
796 symbol: impl Into<String>,
797 period: Period,
798 adjust_type: AdjustType,
799 forward: bool,
800 time: Option<PrimitiveDateTime>,
801 count: usize,
802 trade_sessions: TradeSessions,
803 ) -> Result<Vec<Candlestick>> {
804 let resp: quote::SecurityCandlestickResponse = self
805 .request(
806 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
807 quote::SecurityHistoryCandlestickRequest {
808 symbol: symbol.into(),
809 period: period.into(),
810 adjust_type: adjust_type.into(),
811 query_type: quote::HistoryCandlestickQueryType::QueryByOffset.into(),
812 offset_request: Some(
813 quote::security_history_candlestick_request::OffsetQuery {
814 direction: if forward {
815 quote::Direction::Forward
816 } else {
817 quote::Direction::Backward
818 }
819 .into(),
820 date: time
821 .map(|time| {
822 format!(
823 "{:04}{:02}{:02}",
824 time.year(),
825 time.month() as u8,
826 time.day()
827 )
828 })
829 .unwrap_or_default(),
830 minute: time
831 .map(|time| format!("{:02}{:02}", time.hour(), time.minute()))
832 .unwrap_or_default(),
833 count: count as i32,
834 },
835 ),
836 date_request: None,
837 trade_session: trade_sessions as i32,
838 },
839 )
840 .await?;
841 let candlesticks = resp
842 .candlesticks
843 .into_iter()
844 .map(TryInto::try_into)
845 .collect::<Result<Vec<_>>>()?;
846 Ok(candlesticks)
847 }
848
849 /// Get security history candlesticks by date
850 pub async fn history_candlesticks_by_date(
851 &self,
852 symbol: impl Into<String>,
853 period: Period,
854 adjust_type: AdjustType,
855 start: Option<Date>,
856 end: Option<Date>,
857 trade_sessions: TradeSessions,
858 ) -> Result<Vec<Candlestick>> {
859 let resp: quote::SecurityCandlestickResponse = self
860 .request(
861 cmd_code::GET_SECURITY_HISTORY_CANDLESTICKS,
862 quote::SecurityHistoryCandlestickRequest {
863 symbol: symbol.into(),
864 period: period.into(),
865 adjust_type: adjust_type.into(),
866 query_type: quote::HistoryCandlestickQueryType::QueryByDate.into(),
867 offset_request: None,
868 date_request: Some(quote::security_history_candlestick_request::DateQuery {
869 start_date: start
870 .map(|date| {
871 format!(
872 "{:04}{:02}{:02}",
873 date.year(),
874 date.month() as u8,
875 date.day()
876 )
877 })
878 .unwrap_or_default(),
879 end_date: end
880 .map(|date| {
881 format!(
882 "{:04}{:02}{:02}",
883 date.year(),
884 date.month() as u8,
885 date.day()
886 )
887 })
888 .unwrap_or_default(),
889 }),
890 trade_session: trade_sessions as i32,
891 },
892 )
893 .await?;
894 let candlesticks = resp
895 .candlesticks
896 .into_iter()
897 .map(TryInto::try_into)
898 .collect::<Result<Vec<_>>>()?;
899 Ok(candlesticks)
900 }
901
902 /// Get option chain expiry date list
903 ///
904 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/optionchain-date>
905 ///
906 /// # Examples
907 ///
908 /// ```no_run
909 /// use std::sync::Arc;
910 ///
911 /// use longport::{Config, quote::QuoteContext};
912 ///
913 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
914 /// let config = Arc::new(Config::from_env()?);
915 /// let (ctx, _) = QuoteContext::try_new(config).await?;
916 ///
917 /// let resp = ctx.option_chain_expiry_date_list("AAPL.US").await?;
918 /// println!("{:?}", resp);
919 /// # Ok::<_, Box<dyn std::error::Error>>(())
920 /// # });
921 /// ```
922 pub async fn option_chain_expiry_date_list(
923 &self,
924 symbol: impl Into<String>,
925 ) -> Result<Vec<Date>> {
926 self.0
927 .cache_option_chain_expiry_date_list
928 .get_or_update(symbol.into(), |symbol| async {
929 let resp: quote::OptionChainDateListResponse = self
930 .request(
931 cmd_code::GET_OPTION_CHAIN_EXPIRY_DATE_LIST,
932 quote::SecurityRequest { symbol },
933 )
934 .await?;
935 resp.expiry_date
936 .iter()
937 .map(|value| {
938 parse_date(value).map_err(|err| Error::parse_field_error("date", err))
939 })
940 .collect::<Result<Vec<_>>>()
941 })
942 .await
943 }
944
945 /// Get option chain info by date
946 ///
947 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/optionchain-date-strike>
948 ///
949 /// # Examples
950 ///
951 /// ```no_run
952 /// use std::sync::Arc;
953 ///
954 /// use longport::{Config, quote::QuoteContext};
955 /// use time::macros::date;
956 ///
957 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
958 /// let config = Arc::new(Config::from_env()?);
959 /// let (ctx, _) = QuoteContext::try_new(config).await?;
960 ///
961 /// let resp = ctx
962 /// .option_chain_info_by_date("AAPL.US", date!(2023 - 01 - 20))
963 /// .await?;
964 /// println!("{:?}", resp);
965 /// # Ok::<_, Box<dyn std::error::Error>>(())
966 /// # });
967 /// ```
968 pub async fn option_chain_info_by_date(
969 &self,
970 symbol: impl Into<String>,
971 expiry_date: Date,
972 ) -> Result<Vec<StrikePriceInfo>> {
973 self.0
974 .cache_option_chain_strike_info
975 .get_or_update(
976 (symbol.into(), expiry_date),
977 |(symbol, expiry_date)| async move {
978 let resp: quote::OptionChainDateStrikeInfoResponse = self
979 .request(
980 cmd_code::GET_OPTION_CHAIN_INFO_BY_DATE,
981 quote::OptionChainDateStrikeInfoRequest {
982 symbol,
983 expiry_date: format_date(expiry_date),
984 },
985 )
986 .await?;
987 resp.strike_price_info
988 .into_iter()
989 .map(TryInto::try_into)
990 .collect::<Result<Vec<_>>>()
991 },
992 )
993 .await
994 }
995
996 /// Get warrant issuers
997 ///
998 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/issuer>
999 ///
1000 /// # Examples
1001 ///
1002 /// ```no_run
1003 /// use std::sync::Arc;
1004 ///
1005 /// use longport::{Config, quote::QuoteContext};
1006 ///
1007 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1008 /// let config = Arc::new(Config::from_env()?);
1009 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1010 ///
1011 /// let resp = ctx.warrant_issuers().await?;
1012 /// println!("{:?}", resp);
1013 /// # Ok::<_, Box<dyn std::error::Error>>(())
1014 /// # });
1015 /// ```
1016 pub async fn warrant_issuers(&self) -> Result<Vec<IssuerInfo>> {
1017 self.0
1018 .cache_issuers
1019 .get_or_update(|| async {
1020 let resp = self
1021 .request_without_body::<quote::IssuerInfoResponse>(
1022 cmd_code::GET_WARRANT_ISSUER_IDS,
1023 )
1024 .await?;
1025 Ok(resp.issuer_info.into_iter().map(Into::into).collect())
1026 })
1027 .await
1028 }
1029
1030 /// Query warrant list
1031 #[allow(clippy::too_many_arguments)]
1032 pub async fn warrant_list(
1033 &self,
1034 symbol: impl Into<String>,
1035 sort_by: WarrantSortBy,
1036 sort_order: SortOrderType,
1037 warrant_type: Option<&[WarrantType]>,
1038 issuer: Option<&[i32]>,
1039 expiry_date: Option<&[FilterWarrantExpiryDate]>,
1040 price_type: Option<&[FilterWarrantInOutBoundsType]>,
1041 status: Option<&[WarrantStatus]>,
1042 ) -> Result<Vec<WarrantInfo>> {
1043 let resp = self
1044 .request::<_, quote::WarrantFilterListResponse>(
1045 cmd_code::GET_FILTERED_WARRANT,
1046 quote::WarrantFilterListRequest {
1047 symbol: symbol.into(),
1048 filter_config: Some(quote::FilterConfig {
1049 sort_by: sort_by.into(),
1050 sort_order: sort_order.into(),
1051 sort_offset: 0,
1052 sort_count: 0,
1053 r#type: warrant_type
1054 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1055 .unwrap_or_default(),
1056 issuer: issuer.map(|types| types.to_vec()).unwrap_or_default(),
1057 expiry_date: expiry_date
1058 .map(|e| e.iter().map(|e| (*e).into()).collect())
1059 .unwrap_or_default(),
1060 price_type: price_type
1061 .map(|types| types.iter().map(|ty| (*ty).into()).collect())
1062 .unwrap_or_default(),
1063 status: status
1064 .map(|status| status.iter().map(|status| (*status).into()).collect())
1065 .unwrap_or_default(),
1066 }),
1067 language: self.0.language.into(),
1068 },
1069 )
1070 .await?;
1071 resp.warrant_list
1072 .into_iter()
1073 .map(TryInto::try_into)
1074 .collect::<Result<Vec<_>>>()
1075 }
1076
1077 /// Get trading session of the day
1078 ///
1079 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/trade-session>
1080 ///
1081 /// # Examples
1082 ///
1083 /// ```no_run
1084 /// use std::sync::Arc;
1085 ///
1086 /// use longport::{Config, quote::QuoteContext};
1087 ///
1088 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1089 /// let config = Arc::new(Config::from_env()?);
1090 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1091 ///
1092 /// let resp = ctx.trading_session().await?;
1093 /// println!("{:?}", resp);
1094 /// # Ok::<_, Box<dyn std::error::Error>>(())
1095 /// # });
1096 /// ```
1097 pub async fn trading_session(&self) -> Result<Vec<MarketTradingSession>> {
1098 self.0
1099 .cache_trading_session
1100 .get_or_update(|| async {
1101 let resp = self
1102 .request_without_body::<quote::MarketTradePeriodResponse>(
1103 cmd_code::GET_TRADING_SESSION,
1104 )
1105 .await?;
1106 resp.market_trade_session
1107 .into_iter()
1108 .map(TryInto::try_into)
1109 .collect::<Result<Vec<_>>>()
1110 })
1111 .await
1112 }
1113
1114 /// Get market trading days
1115 ///
1116 /// The interval must be less than one month, and only the most recent year
1117 /// is supported.
1118 ///
1119 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/trade-day>
1120 ///
1121 /// # Examples
1122 ///
1123 /// ```no_run
1124 /// use std::sync::Arc;
1125 ///
1126 /// use longport::{Config, Market, quote::QuoteContext};
1127 /// use time::macros::date;
1128 ///
1129 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1130 /// let config = Arc::new(Config::from_env()?);
1131 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1132 ///
1133 /// let resp = ctx
1134 /// .trading_days(Market::HK, date!(2022 - 01 - 20), date!(2022 - 02 - 20))
1135 /// .await?;
1136 /// println!("{:?}", resp);
1137 /// # Ok::<_, Box<dyn std::error::Error>>(())
1138 /// # });
1139 /// ```
1140 pub async fn trading_days(
1141 &self,
1142 market: Market,
1143 begin: Date,
1144 end: Date,
1145 ) -> Result<MarketTradingDays> {
1146 let resp = self
1147 .request::<_, quote::MarketTradeDayResponse>(
1148 cmd_code::GET_TRADING_DAYS,
1149 quote::MarketTradeDayRequest {
1150 market: market.to_string(),
1151 beg_day: format_date(begin),
1152 end_day: format_date(end),
1153 },
1154 )
1155 .await?;
1156 let trading_days = resp
1157 .trade_day
1158 .iter()
1159 .map(|value| {
1160 parse_date(value).map_err(|err| Error::parse_field_error("trade_day", err))
1161 })
1162 .collect::<Result<Vec<_>>>()?;
1163 let half_trading_days = resp
1164 .half_trade_day
1165 .iter()
1166 .map(|value| {
1167 parse_date(value).map_err(|err| Error::parse_field_error("half_trade_day", err))
1168 })
1169 .collect::<Result<Vec<_>>>()?;
1170 Ok(MarketTradingDays {
1171 trading_days,
1172 half_trading_days,
1173 })
1174 }
1175
1176 /// Get capital flow intraday
1177 ///
1178 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/capital-flow-intraday>
1179 ///
1180 /// # Examples
1181 ///
1182 /// ```no_run
1183 /// use std::sync::Arc;
1184 ///
1185 /// use longport::{quote::QuoteContext, Config};
1186 ///
1187 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1188 /// let config = Arc::new(Config::from_env()?);
1189 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1190 ///
1191 /// let resp = ctx.capital_flow("700.HK").await?;
1192 /// println!("{:?}", resp);
1193 /// # Ok::<_, Box<dyn std::error::Error>>(())
1194 /// # });
1195 pub async fn capital_flow(&self, symbol: impl Into<String>) -> Result<Vec<CapitalFlowLine>> {
1196 self.request::<_, quote::CapitalFlowIntradayResponse>(
1197 cmd_code::GET_CAPITAL_FLOW_INTRADAY,
1198 quote::CapitalFlowIntradayRequest {
1199 symbol: symbol.into(),
1200 },
1201 )
1202 .await?
1203 .capital_flow_lines
1204 .into_iter()
1205 .map(TryInto::try_into)
1206 .collect()
1207 }
1208
1209 /// Get capital distribution
1210 ///
1211 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/capital-distribution>
1212 ///
1213 /// # Examples
1214 ///
1215 /// ```no_run
1216 /// use std::sync::Arc;
1217 ///
1218 /// use longport::{quote::QuoteContext, Config};
1219 ///
1220 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1221 /// let config = Arc::new(Config::from_env()?);
1222 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1223 ///
1224 /// let resp = ctx.capital_distribution("700.HK").await?;
1225 /// println!("{:?}", resp);
1226 /// # Ok::<_, Box<dyn std::error::Error>>(())
1227 /// # });
1228 pub async fn capital_distribution(
1229 &self,
1230 symbol: impl Into<String>,
1231 ) -> Result<CapitalDistributionResponse> {
1232 self.request::<_, quote::CapitalDistributionResponse>(
1233 cmd_code::GET_SECURITY_CAPITAL_DISTRIBUTION,
1234 quote::SecurityRequest {
1235 symbol: symbol.into(),
1236 },
1237 )
1238 .await?
1239 .try_into()
1240 }
1241
1242 /// Get calc indexes
1243 pub async fn calc_indexes<I, T, J>(
1244 &self,
1245 symbols: I,
1246 indexes: J,
1247 ) -> Result<Vec<SecurityCalcIndex>>
1248 where
1249 I: IntoIterator<Item = T>,
1250 T: Into<String>,
1251 J: IntoIterator<Item = CalcIndex>,
1252 {
1253 let indexes = indexes.into_iter().collect::<Vec<CalcIndex>>();
1254 let resp: quote::SecurityCalcQuoteResponse = self
1255 .request(
1256 cmd_code::GET_CALC_INDEXES,
1257 quote::SecurityCalcQuoteRequest {
1258 symbols: symbols.into_iter().map(Into::into).collect(),
1259 calc_index: indexes
1260 .iter()
1261 .map(|i| quote::CalcIndex::from(*i).into())
1262 .collect(),
1263 },
1264 )
1265 .await?;
1266
1267 Ok(resp
1268 .security_calc_index
1269 .into_iter()
1270 .map(|resp| SecurityCalcIndex::from_proto(resp, &indexes))
1271 .collect())
1272 }
1273
1274 /// Get watchlist
1275 ///
1276 /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_groups>
1277 ///
1278 /// # Examples
1279 ///
1280 /// ```no_run
1281 /// use std::sync::Arc;
1282 ///
1283 /// use longport::{Config, quote::QuoteContext};
1284 ///
1285 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1286 /// let config = Arc::new(Config::from_env()?);
1287 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1288 ///
1289 /// let resp = ctx.watchlist().await?;
1290 /// println!("{:?}", resp);
1291 /// # Ok::<_, Box<dyn std::error::Error>>(())
1292 /// # });
1293 /// ```
1294 pub async fn watchlist(&self) -> Result<Vec<WatchlistGroup>> {
1295 #[derive(Debug, Deserialize)]
1296 struct Response {
1297 groups: Vec<WatchlistGroup>,
1298 }
1299
1300 let resp = self
1301 .0
1302 .http_cli
1303 .request(Method::GET, "/v1/watchlist/groups")
1304 .response::<Json<Response>>()
1305 .send()
1306 .with_subscriber(self.0.log_subscriber.clone())
1307 .await?;
1308 Ok(resp.0.groups)
1309 }
1310
1311 /// Create watchlist group
1312 ///
1313 /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_create_group>
1314 ///
1315 /// # Examples
1316 ///
1317 /// ```no_run
1318 /// use std::sync::Arc;
1319 ///
1320 /// use longport::{
1321 /// Config,
1322 /// quote::{QuoteContext, RequestCreateWatchlistGroup},
1323 /// };
1324 ///
1325 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1326 /// let config = Arc::new(Config::from_env()?);
1327 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1328 ///
1329 /// let req = RequestCreateWatchlistGroup::new("Watchlist1").securities(["700.HK", "BABA.US"]);
1330 /// let group_id = ctx.create_watchlist_group(req).await?;
1331 /// println!("{}", group_id);
1332 /// # Ok::<_, Box<dyn std::error::Error>>(())
1333 /// # });
1334 /// ```
1335 pub async fn create_watchlist_group(&self, req: RequestCreateWatchlistGroup) -> Result<i64> {
1336 #[derive(Debug, Serialize)]
1337 struct RequestCreate {
1338 name: String,
1339 #[serde(skip_serializing_if = "Option::is_none")]
1340 securities: Option<Vec<String>>,
1341 }
1342
1343 #[derive(Debug, Deserialize)]
1344 struct Response {
1345 #[serde(with = "serde_utils::int64_str")]
1346 id: i64,
1347 }
1348
1349 let Json(Response { id }) = self
1350 .0
1351 .http_cli
1352 .request(Method::POST, "/v1/watchlist/groups")
1353 .body(Json(RequestCreate {
1354 name: req.name,
1355 securities: req.securities,
1356 }))
1357 .response::<Json<Response>>()
1358 .send()
1359 .with_subscriber(self.0.log_subscriber.clone())
1360 .await?;
1361
1362 Ok(id)
1363 }
1364
1365 /// Delete watchlist group
1366 ///
1367 /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_delete_group>
1368 ///
1369 /// # Examples
1370 ///
1371 /// ```no_run
1372 /// use std::sync::Arc;
1373 ///
1374 /// use longport::{Config, quote::QuoteContext};
1375 ///
1376 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1377 /// let config = Arc::new(Config::from_env()?);
1378 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1379 ///
1380 /// ctx.delete_watchlist_group(10086, true).await?;
1381 /// # Ok::<_, Box<dyn std::error::Error>>(())
1382 /// # });
1383 /// ```
1384 pub async fn delete_watchlist_group(&self, id: i64, purge: bool) -> Result<()> {
1385 #[derive(Debug, Serialize)]
1386 struct Request {
1387 id: i64,
1388 purge: bool,
1389 }
1390
1391 Ok(self
1392 .0
1393 .http_cli
1394 .request(Method::DELETE, "/v1/watchlist/groups")
1395 .query_params(Request { id, purge })
1396 .send()
1397 .with_subscriber(self.0.log_subscriber.clone())
1398 .await?)
1399 }
1400
1401 /// Update watchlist group
1402 ///
1403 /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_update_group>
1404 /// Reference: <https://open.longportapp.com/en/docs/quote/individual/watchlist_update_group_securities>
1405 ///
1406 /// # Examples
1407 ///
1408 /// ```no_run
1409 /// use std::sync::Arc;
1410 ///
1411 /// use longport::{
1412 /// Config,
1413 /// quote::{QuoteContext, RequestUpdateWatchlistGroup},
1414 /// };
1415 ///
1416 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1417 /// let config = Arc::new(Config::from_env()?);
1418 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1419 /// let req = RequestUpdateWatchlistGroup::new(10086)
1420 /// .name("Watchlist2")
1421 /// .securities(["700.HK", "BABA.US"]);
1422 /// ctx.update_watchlist_group(req).await?;
1423 /// # Ok::<_, Box<dyn std::error::Error>>(())
1424 /// # });
1425 /// ```
1426 pub async fn update_watchlist_group(&self, req: RequestUpdateWatchlistGroup) -> Result<()> {
1427 #[derive(Debug, Serialize)]
1428 struct RequestUpdate {
1429 id: i64,
1430 #[serde(skip_serializing_if = "Option::is_none")]
1431 name: Option<String>,
1432 #[serde(skip_serializing_if = "Option::is_none")]
1433 securities: Option<Vec<String>>,
1434 #[serde(skip_serializing_if = "Option::is_none")]
1435 mode: Option<SecuritiesUpdateMode>,
1436 }
1437
1438 self.0
1439 .http_cli
1440 .request(Method::PUT, "/v1/watchlist/groups")
1441 .body(Json(RequestUpdate {
1442 id: req.id,
1443 name: req.name,
1444 mode: req.securities.is_some().then_some(req.mode),
1445 securities: req.securities,
1446 }))
1447 .send()
1448 .with_subscriber(self.0.log_subscriber.clone())
1449 .await?;
1450
1451 Ok(())
1452 }
1453
1454 /// Get security list
1455 pub async fn security_list(
1456 &self,
1457 market: Market,
1458 category: impl Into<Option<SecurityListCategory>>,
1459 ) -> Result<Vec<Security>> {
1460 #[derive(Debug, Serialize)]
1461 struct Request {
1462 market: Market,
1463 #[serde(skip_serializing_if = "Option::is_none")]
1464 category: Option<SecurityListCategory>,
1465 }
1466
1467 #[derive(Debug, Deserialize)]
1468 struct Response {
1469 list: Vec<Security>,
1470 }
1471
1472 Ok(self
1473 .0
1474 .http_cli
1475 .request(Method::GET, "/v1/quote/get_security_list")
1476 .query_params(Request {
1477 market,
1478 category: category.into(),
1479 })
1480 .response::<Json<Response>>()
1481 .send()
1482 .with_subscriber(self.0.log_subscriber.clone())
1483 .await?
1484 .0
1485 .list)
1486 }
1487
1488 /// Get current market temperature
1489 ///
1490 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/market_temperature>
1491 ///
1492 /// # Examples
1493 ///
1494 /// ```no_run
1495 /// use std::sync::Arc;
1496 ///
1497 /// use longport::{Config, Market, quote::QuoteContext};
1498 ///
1499 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1500 /// let config = Arc::new(Config::from_env()?);
1501 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1502 ///
1503 /// let resp = ctx.market_temperature(Market::HK).await?;
1504 /// println!("{:?}", resp);
1505 /// # Ok::<_, Box<dyn std::error::Error>>(())
1506 /// # });
1507 /// ```
1508 pub async fn market_temperature(&self, market: Market) -> Result<MarketTemperature> {
1509 #[derive(Debug, Serialize)]
1510 struct Request {
1511 market: Market,
1512 }
1513
1514 Ok(self
1515 .0
1516 .http_cli
1517 .request(Method::GET, "/v1/quote/market_temperature")
1518 .query_params(Request { market })
1519 .response::<Json<MarketTemperature>>()
1520 .send()
1521 .with_subscriber(self.0.log_subscriber.clone())
1522 .await?
1523 .0)
1524 }
1525
1526 /// Get historical market temperature
1527 ///
1528 /// Reference: <https://open.longportapp.com/en/docs/quote/pull/history_market_temperature>
1529 ///
1530 /// # Examples
1531 ///
1532 /// ```no_run
1533 /// use std::sync::Arc;
1534 ///
1535 /// use longport::{Config, Market, quote::QuoteContext};
1536 /// use time::macros::date;
1537 ///
1538 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1539 /// let config = Arc::new(Config::from_env()?);
1540 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1541 ///
1542 /// let resp = ctx
1543 /// .history_market_temperature(Market::HK, date!(2023 - 01 - 01), date!(2023 - 01 - 31))
1544 /// .await?;
1545 /// println!("{:?}", resp);
1546 /// # Ok::<_, Box<dyn std::error::Error>>(())
1547 /// # });
1548 /// ```
1549 pub async fn history_market_temperature(
1550 &self,
1551 market: Market,
1552 start_date: Date,
1553 end_date: Date,
1554 ) -> Result<HistoryMarketTemperatureResponse> {
1555 #[derive(Debug, Serialize)]
1556 struct Request {
1557 market: Market,
1558 start_date: String,
1559 end_date: String,
1560 }
1561
1562 Ok(self
1563 .0
1564 .http_cli
1565 .request(Method::GET, "/v1/quote/history_market_temperature")
1566 .query_params(Request {
1567 market,
1568 start_date: format_date(start_date),
1569 end_date: format_date(end_date),
1570 })
1571 .response::<Json<HistoryMarketTemperatureResponse>>()
1572 .send()
1573 .with_subscriber(self.0.log_subscriber.clone())
1574 .await?
1575 .0)
1576 }
1577
1578 /// Get real-time quotes
1579 ///
1580 /// Get real-time quotes of the subscribed symbols, it always returns the
1581 /// data in the local storage.
1582 ///
1583 /// # Examples
1584 ///
1585 /// ```no_run
1586 /// use std::{sync::Arc, time::Duration};
1587 ///
1588 /// use longport::{
1589 /// Config,
1590 /// quote::{QuoteContext, SubFlags},
1591 /// };
1592 ///
1593 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1594 /// let config = Arc::new(Config::from_env()?);
1595 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1596 ///
1597 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::QUOTE, true)
1598 /// .await?;
1599 /// tokio::time::sleep(Duration::from_secs(5)).await;
1600 ///
1601 /// let resp = ctx.realtime_quote(["700.HK", "AAPL.US"]).await?;
1602 /// println!("{:?}", resp);
1603 /// # Ok::<_, Box<dyn std::error::Error>>(())
1604 /// # });
1605 /// ```
1606 pub async fn realtime_quote<I, T>(&self, symbols: I) -> Result<Vec<RealtimeQuote>>
1607 where
1608 I: IntoIterator<Item = T>,
1609 T: Into<String>,
1610 {
1611 let (reply_tx, reply_rx) = oneshot::channel();
1612 self.0
1613 .command_tx
1614 .send(Command::GetRealtimeQuote {
1615 symbols: symbols.into_iter().map(Into::into).collect(),
1616 reply_tx,
1617 })
1618 .map_err(|_| WsClientError::ClientClosed)?;
1619 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1620 }
1621
1622 /// Get real-time depth
1623 ///
1624 /// Get real-time depth of the subscribed symbols, it always returns the
1625 /// data in the local storage.
1626 ///
1627 /// # Examples
1628 ///
1629 /// ```no_run
1630 /// use std::{sync::Arc, time::Duration};
1631 ///
1632 /// use longport::{
1633 /// Config,
1634 /// quote::{QuoteContext, SubFlags},
1635 /// };
1636 ///
1637 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1638 /// let config = Arc::new(Config::from_env()?);
1639 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1640 ///
1641 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::DEPTH, true)
1642 /// .await?;
1643 /// tokio::time::sleep(Duration::from_secs(5)).await;
1644 ///
1645 /// let resp = ctx.realtime_depth("700.HK").await?;
1646 /// println!("{:?}", resp);
1647 /// # Ok::<_, Box<dyn std::error::Error>>(())
1648 /// # });
1649 /// ```
1650 pub async fn realtime_depth(&self, symbol: impl Into<String>) -> Result<SecurityDepth> {
1651 let (reply_tx, reply_rx) = oneshot::channel();
1652 self.0
1653 .command_tx
1654 .send(Command::GetRealtimeDepth {
1655 symbol: symbol.into(),
1656 reply_tx,
1657 })
1658 .map_err(|_| WsClientError::ClientClosed)?;
1659 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1660 }
1661
1662 /// Get real-time trades
1663 ///
1664 /// Get real-time trades of the subscribed symbols, it always returns the
1665 /// data in the local storage.
1666 ///
1667 /// # Examples
1668 ///
1669 /// ```no_run
1670 /// use std::{sync::Arc, time::Duration};
1671 ///
1672 /// use longport::{
1673 /// Config,
1674 /// quote::{QuoteContext, SubFlags},
1675 /// };
1676 ///
1677 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1678 /// let config = Arc::new(Config::from_env()?);
1679 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1680 ///
1681 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::TRADE, false)
1682 /// .await?;
1683 /// tokio::time::sleep(Duration::from_secs(5)).await;
1684 ///
1685 /// let resp = ctx.realtime_trades("700.HK", 10).await?;
1686 /// println!("{:?}", resp);
1687 /// # Ok::<_, Box<dyn std::error::Error>>(())
1688 /// # });
1689 /// ```
1690 pub async fn realtime_trades(
1691 &self,
1692 symbol: impl Into<String>,
1693 count: usize,
1694 ) -> Result<Vec<Trade>> {
1695 let (reply_tx, reply_rx) = oneshot::channel();
1696 self.0
1697 .command_tx
1698 .send(Command::GetRealtimeTrade {
1699 symbol: symbol.into(),
1700 count,
1701 reply_tx,
1702 })
1703 .map_err(|_| WsClientError::ClientClosed)?;
1704 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1705 }
1706
1707 /// Get real-time broker queue
1708 ///
1709 ///
1710 /// Get real-time broker queue of the subscribed symbols, it always returns
1711 /// the data in the local storage.
1712 ///
1713 /// # Examples
1714 ///
1715 /// ```no_run
1716 /// use std::{sync::Arc, time::Duration};
1717 ///
1718 /// use longport::{
1719 /// Config,
1720 /// quote::{QuoteContext, SubFlags},
1721 /// };
1722 ///
1723 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1724 /// let config = Arc::new(Config::from_env()?);
1725 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1726 ///
1727 /// ctx.subscribe(["700.HK", "AAPL.US"], SubFlags::BROKER, true)
1728 /// .await?;
1729 /// tokio::time::sleep(Duration::from_secs(5)).await;
1730 ///
1731 /// let resp = ctx.realtime_brokers("700.HK").await?;
1732 /// println!("{:?}", resp);
1733 /// # Ok::<_, Box<dyn std::error::Error>>(())
1734 /// # });
1735 /// ```
1736 pub async fn realtime_brokers(&self, symbol: impl Into<String>) -> Result<SecurityBrokers> {
1737 let (reply_tx, reply_rx) = oneshot::channel();
1738 self.0
1739 .command_tx
1740 .send(Command::GetRealtimeBrokers {
1741 symbol: symbol.into(),
1742 reply_tx,
1743 })
1744 .map_err(|_| WsClientError::ClientClosed)?;
1745 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1746 }
1747
1748 /// Get real-time candlesticks
1749 ///
1750 /// Get real-time candlesticks of the subscribed symbols, it always returns
1751 /// the data in the local storage.
1752 ///
1753 /// # Examples
1754 ///
1755 /// ```no_run
1756 /// use std::{sync::Arc, time::Duration};
1757 ///
1758 /// use longport::{
1759 /// Config,
1760 /// quote::{Period, QuoteContext, TradeSessions},
1761 /// };
1762 ///
1763 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
1764 /// let config = Arc::new(Config::from_env()?);
1765 /// let (ctx, _) = QuoteContext::try_new(config).await?;
1766 ///
1767 /// ctx.subscribe_candlesticks("AAPL.US", Period::OneMinute, TradeSessions::Intraday)
1768 /// .await?;
1769 /// tokio::time::sleep(Duration::from_secs(5)).await;
1770 ///
1771 /// let resp = ctx
1772 /// .realtime_candlesticks("AAPL.US", Period::OneMinute, 10)
1773 /// .await?;
1774 /// println!("{:?}", resp);
1775 /// # Ok::<_, Box<dyn std::error::Error>>(())
1776 /// # });
1777 /// ```
1778 pub async fn realtime_candlesticks(
1779 &self,
1780 symbol: impl Into<String>,
1781 period: Period,
1782 count: usize,
1783 ) -> Result<Vec<Candlestick>> {
1784 let (reply_tx, reply_rx) = oneshot::channel();
1785 self.0
1786 .command_tx
1787 .send(Command::GetRealtimeCandlesticks {
1788 symbol: symbol.into(),
1789 period,
1790 count,
1791 reply_tx,
1792 })
1793 .map_err(|_| WsClientError::ClientClosed)?;
1794 Ok(reply_rx.await.map_err(|_| WsClientError::ClientClosed)?)
1795 }
1796}
1797
1798fn normalize_symbol(symbol: &str) -> &str {
1799 match symbol.split_once('.') {
1800 Some((_, market)) if market.eq_ignore_ascii_case("HK") => symbol.trim_start_matches('0'),
1801 _ => symbol,
1802 }
1803}