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