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