longport/quote/
types.rs

1use longport_candlesticks::CandlestickComponents;
2use longport_proto::quote::{self, Period, TradeStatus};
3use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use strum_macros::{Display, EnumString};
7use time::{Date, OffsetDateTime, Time};
8
9use crate::{
10    Error, Market, Result,
11    quote::{SubFlags, utils::parse_date},
12    serde_utils,
13};
14
15/// Trade session type
16#[derive(Debug, Default, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
17pub enum TradeSession {
18    /// Intraday
19    #[default]
20    Intraday,
21    /// Pre-market
22    Pre,
23    /// Post-market
24    Post,
25    /// Overnight
26    Overnight,
27}
28
29impl longport_candlesticks::TradeSessionType for TradeSession {
30    #[inline]
31    fn kind(&self) -> longport_candlesticks::TradeSessionKind {
32        match self {
33            TradeSession::Intraday => longport_candlesticks::TRADE_SESSION_INTRADAY,
34            TradeSession::Pre => longport_candlesticks::TRADE_SESSION_PRE,
35            TradeSession::Post => longport_candlesticks::TRADE_SESSION_POST,
36            TradeSession::Overnight => longport_candlesticks::TRADE_SESSION_OVERNIGHT,
37        }
38    }
39}
40
41impl From<longport_proto::quote::TradeSession> for TradeSession {
42    #[inline]
43    fn from(value: longport_proto::quote::TradeSession) -> Self {
44        match value {
45            longport_proto::quote::TradeSession::NormalTrade => Self::Intraday,
46            longport_proto::quote::TradeSession::PreTrade => Self::Pre,
47            longport_proto::quote::TradeSession::PostTrade => Self::Post,
48            longport_proto::quote::TradeSession::OvernightTrade => Self::Overnight,
49        }
50    }
51}
52
53/// Subscription
54#[derive(Debug, Clone)]
55pub struct Subscription {
56    /// Security code
57    pub symbol: String,
58    /// Subscription flags
59    pub sub_types: SubFlags,
60    /// Candlesticks
61    pub candlesticks: Vec<Period>,
62}
63
64/// Depth
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Depth {
67    /// Position
68    pub position: i32,
69    /// Price
70    pub price: Option<Decimal>,
71    /// Volume
72    pub volume: i64,
73    /// Number of orders
74    pub order_num: i64,
75}
76
77impl TryFrom<quote::Depth> for Depth {
78    type Error = Error;
79
80    fn try_from(depth: quote::Depth) -> Result<Self> {
81        Ok(Self {
82            position: depth.position,
83            price: depth.price.parse().ok(),
84            volume: depth.volume,
85            order_num: depth.order_num,
86        })
87    }
88}
89
90/// Brokers
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct Brokers {
93    /// Position
94    pub position: i32,
95    /// Broker IDs
96    pub broker_ids: Vec<i32>,
97}
98
99impl From<quote::Brokers> for Brokers {
100    fn from(brokers: quote::Brokers) -> Self {
101        Self {
102            position: brokers.position,
103            broker_ids: brokers.broker_ids,
104        }
105    }
106}
107
108/// Trade direction
109#[derive(Debug, FromPrimitive, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
110#[repr(i32)]
111pub enum TradeDirection {
112    /// Neutral
113    #[num_enum(default)]
114    Neutral = 0,
115    /// Down
116    Down = 1,
117    /// Up
118    Up = 2,
119}
120
121/// Trade
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Trade {
124    /// Price
125    pub price: Decimal,
126    /// Volume
127    pub volume: i64,
128    /// Time of trading
129    #[serde(with = "time::serde::rfc3339")]
130    pub timestamp: OffsetDateTime,
131    /// Trade type
132    ///
133    /// HK
134    ///
135    /// - `*` - Overseas trade
136    /// - `D` - Odd-lot trade
137    /// - `M` - Non-direct off-exchange trade
138    /// - `P` - Late trade (Off-exchange previous day)
139    /// - `U` - Auction trade
140    /// - `X` - Direct off-exchange trade
141    /// - `Y` - Automatch internalized
142    /// - `<empty string>` -  Automatch normal
143    ///
144    /// US
145    ///
146    /// - `<empty string>` - Regular sale
147    /// - `A` - Acquisition
148    /// - `B` - Bunched trade
149    /// - `D` - Distribution
150    /// - `F` - Intermarket sweep
151    /// - `G` - Bunched sold trades
152    /// - `H` - Price variation trade
153    /// - `I` - Odd lot trade
154    /// - `K` - Rule 155 trde(NYSE MKT)
155    /// - `M` - Market center close price
156    /// - `P` - Prior reference price
157    /// - `Q` - Market center open price
158    /// - `S` - Split trade
159    /// - `V` - Contingent trade
160    /// - `W` - Average price trade
161    /// - `X` - Cross trade
162    /// - `1` - Stopped stock(Regular trade)
163    pub trade_type: String,
164    /// Trade direction
165    pub direction: TradeDirection,
166    /// Trade session
167    pub trade_session: TradeSession,
168}
169
170impl TryFrom<quote::Trade> for Trade {
171    type Error = Error;
172
173    fn try_from(trade: quote::Trade) -> Result<Self> {
174        Ok(Self {
175            price: trade.price.parse().unwrap_or_default(),
176            volume: trade.volume,
177            timestamp: OffsetDateTime::from_unix_timestamp(trade.timestamp)
178                .map_err(|err| Error::parse_field_error("timestamp", err))?,
179            trade_type: trade.trade_type,
180            direction: trade.direction.into(),
181            trade_session: longport_proto::quote::TradeSession::try_from(trade.trade_session)
182                .unwrap_or_default()
183                .into(),
184        })
185    }
186}
187
188impl longport_candlesticks::TradeType for Trade {
189    type PriceType = Decimal;
190    type VolumeType = i64;
191    type TurnoverType = Decimal;
192    type TradeSessionType = TradeSession;
193
194    #[inline]
195    fn time(&self) -> OffsetDateTime {
196        self.timestamp
197    }
198
199    #[inline]
200    fn price(&self) -> Self::PriceType {
201        self.price
202    }
203
204    #[inline]
205    fn volume(&self) -> Self::VolumeType {
206        self.volume
207    }
208
209    #[inline]
210    fn turnover(&self, lot_size: i32) -> Self::TurnoverType {
211        self.price * Decimal::from(self.volume * lot_size as i64)
212    }
213
214    #[inline]
215    fn trade_session(&self) -> TradeSession {
216        self.trade_session
217    }
218}
219
220bitflags::bitflags! {
221    /// Derivative type
222    #[derive(Debug, Copy, Clone, Serialize,Deserialize)]
223    pub struct DerivativeType: u8 {
224        /// US stock options
225        const OPTION = 0x1;
226
227        /// HK warrants
228        const WARRANT = 0x2;
229    }
230}
231
232/// Security board
233#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display, Serialize, Deserialize)]
234#[allow(clippy::upper_case_acronyms)]
235pub enum SecurityBoard {
236    /// Unknown
237    Unknown,
238    /// US Main Board
239    USMain,
240    /// US Pink Board
241    USPink,
242    /// Dow Jones Industrial Average
243    USDJI,
244    /// Nasdsaq Index
245    USNSDQ,
246    /// US Industry Board
247    USSector,
248    /// US Option
249    USOption,
250    /// US Sepecial Option
251    USOptionS,
252    /// Hong Kong Equity Securities
253    HKEquity,
254    /// HK PreIPO Security
255    HKPreIPO,
256    /// HK Warrant
257    HKWarrant,
258    /// Hang Seng Index
259    HKHS,
260    /// HK Industry Board
261    HKSector,
262    /// SH Main Board(Connect)
263    SHMainConnect,
264    /// SH Main Board(Non Connect)
265    SHMainNonConnect,
266    /// SH Science and Technology Innovation Board
267    SHSTAR,
268    /// CN Index
269    CNIX,
270    /// CN Industry Board
271    CNSector,
272    /// SZ Main Board(Connect)
273    SZMainConnect,
274    /// SZ Main Board(Non Connect)
275    SZMainNonConnect,
276    /// SZ Gem Board(Connect)
277    SZGEMConnect,
278    /// SZ Gem Board(Non Connect)
279    SZGEMNonConnect,
280    /// SG Main Board
281    SGMain,
282    /// Singapore Straits Index
283    STI,
284    /// SG Industry Board
285    SGSector,
286}
287
288/// The basic information of securities
289#[derive(Debug, Serialize, Deserialize)]
290pub struct SecurityStaticInfo {
291    /// Security code
292    pub symbol: String,
293    /// Security name (zh-CN)
294    pub name_cn: String,
295    /// Security name (en)
296    pub name_en: String,
297    /// Security name (zh-HK)
298    pub name_hk: String,
299    /// Exchange which the security belongs to
300    pub exchange: String,
301    /// Trading currency
302    pub currency: String,
303    /// Lot size
304    pub lot_size: i32,
305    /// Total shares
306    pub total_shares: i64,
307    /// Circulating shares
308    pub circulating_shares: i64,
309    /// HK shares (only HK stocks)
310    pub hk_shares: i64,
311    /// Earnings per share
312    pub eps: Decimal,
313    /// Earnings per share (TTM)
314    pub eps_ttm: Decimal,
315    /// Net assets per share
316    pub bps: Decimal,
317    /// Dividend yield
318    pub dividend_yield: Decimal,
319    /// Types of supported derivatives
320    pub stock_derivatives: DerivativeType,
321    /// Board
322    pub board: SecurityBoard,
323}
324
325impl TryFrom<quote::StaticInfo> for SecurityStaticInfo {
326    type Error = Error;
327
328    fn try_from(resp: quote::StaticInfo) -> Result<Self> {
329        Ok(SecurityStaticInfo {
330            symbol: resp.symbol,
331            name_cn: resp.name_cn,
332            name_en: resp.name_en,
333            name_hk: resp.name_hk,
334            exchange: resp.exchange,
335            currency: resp.currency,
336            lot_size: resp.lot_size,
337            total_shares: resp.total_shares,
338            circulating_shares: resp.circulating_shares,
339            hk_shares: resp.hk_shares,
340            eps: resp.eps.parse().unwrap_or_default(),
341            eps_ttm: resp.eps_ttm.parse().unwrap_or_default(),
342            bps: resp.bps.parse().unwrap_or_default(),
343            dividend_yield: resp.dividend_yield.parse().unwrap_or_default(),
344            stock_derivatives: resp.stock_derivatives.into_iter().fold(
345                DerivativeType::empty(),
346                |acc, value| match value {
347                    1 => acc | DerivativeType::OPTION,
348                    2 => acc | DerivativeType::WARRANT,
349                    _ => acc,
350                },
351            ),
352            board: resp.board.parse().unwrap_or(SecurityBoard::Unknown),
353        })
354    }
355}
356
357/// Real-time quote
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct RealtimeQuote {
360    /// Security code
361    pub symbol: String,
362    /// Latest price
363    pub last_done: Decimal,
364    /// Open
365    pub open: Decimal,
366    /// High
367    pub high: Decimal,
368    /// Low
369    pub low: Decimal,
370    /// Time of latest price
371    pub timestamp: OffsetDateTime,
372    /// Volume
373    pub volume: i64,
374    /// Turnover
375    pub turnover: Decimal,
376    /// Security trading status
377    pub trade_status: TradeStatus,
378}
379
380/// Quote of US pre/post market
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct PrePostQuote {
383    /// Latest price
384    pub last_done: Decimal,
385    /// Time of latest price
386    #[serde(with = "time::serde::rfc3339")]
387    pub timestamp: OffsetDateTime,
388    /// Volume
389    pub volume: i64,
390    /// Turnover
391    pub turnover: Decimal,
392    /// High
393    pub high: Decimal,
394    /// Low
395    pub low: Decimal,
396    /// Close of the last trade session
397    pub prev_close: Decimal,
398}
399
400impl TryFrom<quote::PrePostQuote> for PrePostQuote {
401    type Error = Error;
402
403    fn try_from(quote: quote::PrePostQuote) -> Result<Self> {
404        Ok(Self {
405            last_done: quote.last_done.parse().unwrap_or_default(),
406            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
407                .map_err(|err| Error::parse_field_error("timestamp", err))?,
408            volume: quote.volume,
409            turnover: quote.turnover.parse().unwrap_or_default(),
410            high: quote.high.parse().unwrap_or_default(),
411            low: quote.low.parse().unwrap_or_default(),
412            prev_close: quote.prev_close.parse().unwrap_or_default(),
413        })
414    }
415}
416
417/// Quote of securitity
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct SecurityQuote {
420    /// Security code
421    pub symbol: String,
422    /// Latest price
423    pub last_done: Decimal,
424    /// Yesterday's close
425    pub prev_close: Decimal,
426    /// Open
427    pub open: Decimal,
428    /// High
429    pub high: Decimal,
430    /// Low
431    pub low: Decimal,
432    /// Time of latest price
433    #[serde(with = "time::serde::rfc3339")]
434    pub timestamp: OffsetDateTime,
435    /// Volume
436    pub volume: i64,
437    /// Turnover
438    pub turnover: Decimal,
439    /// Security trading status
440    pub trade_status: TradeStatus,
441    /// Quote of US pre market
442    pub pre_market_quote: Option<PrePostQuote>,
443    /// Quote of US post market
444    pub post_market_quote: Option<PrePostQuote>,
445    /// Quote of US overnight market
446    pub overnight_quote: Option<PrePostQuote>,
447}
448
449impl TryFrom<quote::SecurityQuote> for SecurityQuote {
450    type Error = Error;
451
452    fn try_from(quote: quote::SecurityQuote) -> Result<Self> {
453        Ok(Self {
454            symbol: quote.symbol,
455            last_done: quote.last_done.parse().unwrap_or_default(),
456            prev_close: quote.prev_close.parse().unwrap_or_default(),
457            open: quote.open.parse().unwrap_or_default(),
458            high: quote.high.parse().unwrap_or_default(),
459            low: quote.low.parse().unwrap_or_default(),
460            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
461                .map_err(|err| Error::parse_field_error("timestamp", err))?,
462            volume: quote.volume,
463            turnover: quote.turnover.parse().unwrap_or_default(),
464            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
465            pre_market_quote: quote.pre_market_quote.map(TryInto::try_into).transpose()?,
466            post_market_quote: quote.post_market_quote.map(TryInto::try_into).transpose()?,
467            overnight_quote: quote.over_night_quote.map(TryInto::try_into).transpose()?,
468        })
469    }
470}
471
472/// Option type
473#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
474pub enum OptionType {
475    /// Unknown
476    Unknown,
477    /// American
478    #[strum(serialize = "A")]
479    American,
480    /// Europe
481    #[strum(serialize = "U")]
482    Europe,
483}
484
485/// Option direction
486#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
487pub enum OptionDirection {
488    /// Unknown
489    Unknown,
490    /// Put
491    #[strum(serialize = "P")]
492    Put,
493    /// Call
494    #[strum(serialize = "C")]
495    Call,
496}
497
498/// Quote of option
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct OptionQuote {
501    /// Security code
502    pub symbol: String,
503    /// Latest price
504    pub last_done: Decimal,
505    /// Yesterday's close
506    pub prev_close: Decimal,
507    /// Open
508    pub open: Decimal,
509    /// High
510    pub high: Decimal,
511    /// Low
512    pub low: Decimal,
513    /// Time of latest price
514    #[serde(with = "time::serde::rfc3339")]
515    pub timestamp: OffsetDateTime,
516    /// Volume
517    pub volume: i64,
518    /// Turnover
519    pub turnover: Decimal,
520    /// Security trading status
521    pub trade_status: TradeStatus,
522    /// Implied volatility
523    pub implied_volatility: Decimal,
524    /// Number of open positions
525    pub open_interest: i64,
526    /// Exprity date
527    pub expiry_date: Date,
528    /// Strike price
529    pub strike_price: Decimal,
530    /// Contract multiplier
531    pub contract_multiplier: Decimal,
532    /// Option type
533    pub contract_type: OptionType,
534    /// Contract size
535    pub contract_size: Decimal,
536    /// Option direction
537    pub direction: OptionDirection,
538    /// Underlying security historical volatility of the option
539    pub historical_volatility: Decimal,
540    /// Underlying security symbol of the option
541    pub underlying_symbol: String,
542}
543
544impl TryFrom<quote::OptionQuote> for OptionQuote {
545    type Error = Error;
546
547    fn try_from(quote: quote::OptionQuote) -> Result<Self> {
548        let option_extend = quote.option_extend.unwrap_or_default();
549
550        Ok(Self {
551            symbol: quote.symbol,
552            last_done: quote.last_done.parse().unwrap_or_default(),
553            prev_close: quote.prev_close.parse().unwrap_or_default(),
554            open: quote.open.parse().unwrap_or_default(),
555            high: quote.high.parse().unwrap_or_default(),
556            low: quote.low.parse().unwrap_or_default(),
557            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
558                .map_err(|err| Error::parse_field_error("timestamp", err))?,
559            volume: quote.volume,
560            turnover: quote.turnover.parse().unwrap_or_default(),
561            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
562            implied_volatility: option_extend.implied_volatility.parse().unwrap_or_default(),
563            open_interest: option_extend.open_interest,
564            expiry_date: parse_date(&option_extend.expiry_date)
565                .map_err(|err| Error::parse_field_error("expiry_date", err))?,
566            strike_price: option_extend.strike_price.parse().unwrap_or_default(),
567            contract_multiplier: option_extend
568                .contract_multiplier
569                .parse()
570                .unwrap_or_default(),
571            contract_type: option_extend.contract_type.parse().unwrap_or_default(),
572            contract_size: option_extend.contract_size.parse().unwrap_or_default(),
573            direction: option_extend.direction.parse().unwrap_or_default(),
574            historical_volatility: option_extend
575                .historical_volatility
576                .parse()
577                .unwrap_or_default(),
578            underlying_symbol: option_extend.underlying_symbol,
579        })
580    }
581}
582
583/// Warrant type
584#[derive(
585    Debug,
586    Copy,
587    Clone,
588    Hash,
589    Eq,
590    PartialEq,
591    EnumString,
592    IntoPrimitive,
593    TryFromPrimitive,
594    Serialize,
595    Deserialize,
596)]
597#[repr(i32)]
598pub enum WarrantType {
599    /// Unknown
600    Unknown = -1,
601    /// Call
602    Call = 0,
603    /// Put
604    Put = 1,
605    /// Bull
606    Bull = 2,
607    /// Bear
608    Bear = 3,
609    /// Inline
610    Inline = 4,
611}
612
613/// Quote of warrant
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct WarrantQuote {
616    /// Security code
617    pub symbol: String,
618    /// Latest price
619    pub last_done: Decimal,
620    /// Yesterday's close
621    pub prev_close: Decimal,
622    /// Open
623    pub open: Decimal,
624    /// High
625    pub high: Decimal,
626    /// Low
627    pub low: Decimal,
628    /// Time of latest price
629    #[serde(with = "time::serde::rfc3339")]
630    pub timestamp: OffsetDateTime,
631    /// Volume
632    pub volume: i64,
633    /// Turnover
634    pub turnover: Decimal,
635    /// Security trading status
636    pub trade_status: TradeStatus,
637    /// Implied volatility
638    pub implied_volatility: Decimal,
639    /// Exprity date
640    pub expiry_date: Date,
641    /// Last tradalbe date
642    pub last_trade_date: Date,
643    /// Outstanding ratio
644    pub outstanding_ratio: Decimal,
645    /// Outstanding quantity
646    pub outstanding_quantity: i64,
647    /// Conversion ratio
648    pub conversion_ratio: Decimal,
649    /// Warrant type
650    pub category: WarrantType,
651    /// Strike price
652    pub strike_price: Decimal,
653    /// Upper bound price
654    pub upper_strike_price: Decimal,
655    /// Lower bound price
656    pub lower_strike_price: Decimal,
657    /// Call price
658    pub call_price: Decimal,
659    /// Underlying security symbol of the warrant
660    pub underlying_symbol: String,
661}
662
663impl TryFrom<quote::WarrantQuote> for WarrantQuote {
664    type Error = Error;
665
666    fn try_from(quote: quote::WarrantQuote) -> Result<Self> {
667        let warrant_extend = quote.warrant_extend.unwrap_or_default();
668
669        Ok(Self {
670            symbol: quote.symbol,
671            last_done: quote.last_done.parse().unwrap_or_default(),
672            prev_close: quote.prev_close.parse().unwrap_or_default(),
673            open: quote.open.parse().unwrap_or_default(),
674            high: quote.high.parse().unwrap_or_default(),
675            low: quote.low.parse().unwrap_or_default(),
676            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
677                .map_err(|err| Error::parse_field_error("timestamp", err))?,
678            volume: quote.volume,
679            turnover: quote.turnover.parse().unwrap_or_default(),
680            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
681            implied_volatility: warrant_extend
682                .implied_volatility
683                .parse()
684                .unwrap_or_default(),
685            expiry_date: parse_date(&warrant_extend.expiry_date)
686                .map_err(|err| Error::parse_field_error("expiry_date", err))?,
687            last_trade_date: parse_date(&warrant_extend.last_trade_date)
688                .map_err(|err| Error::parse_field_error("last_trade_date", err))?,
689            outstanding_ratio: warrant_extend.outstanding_ratio.parse().unwrap_or_default(),
690            outstanding_quantity: warrant_extend.outstanding_qty,
691            conversion_ratio: warrant_extend.conversion_ratio.parse().unwrap_or_default(),
692            category: warrant_extend.category.parse().unwrap_or_default(),
693            strike_price: warrant_extend.strike_price.parse().unwrap_or_default(),
694            upper_strike_price: warrant_extend
695                .upper_strike_price
696                .parse()
697                .unwrap_or_default(),
698            lower_strike_price: warrant_extend
699                .lower_strike_price
700                .parse()
701                .unwrap_or_default(),
702            call_price: warrant_extend.call_price.parse().unwrap_or_default(),
703            underlying_symbol: warrant_extend.underlying_symbol,
704        })
705    }
706}
707
708/// Security depth
709#[derive(Debug, Clone, Default, Serialize, Deserialize)]
710pub struct SecurityDepth {
711    /// Ask depth
712    pub asks: Vec<Depth>,
713    /// Bid depth
714    pub bids: Vec<Depth>,
715}
716
717/// Security brokers
718#[derive(Debug, Clone, Default, Serialize, Deserialize)]
719pub struct SecurityBrokers {
720    /// Ask brokers
721    pub ask_brokers: Vec<Brokers>,
722    /// Bid brokers
723    pub bid_brokers: Vec<Brokers>,
724}
725
726/// Participant info
727#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct ParticipantInfo {
729    /// Broker IDs
730    pub broker_ids: Vec<i32>,
731    /// Participant name (zh-CN)
732    pub name_cn: String,
733    /// Participant name (en)
734    pub name_en: String,
735    /// Participant name (zh-HK)
736    pub name_hk: String,
737}
738
739impl From<quote::ParticipantInfo> for ParticipantInfo {
740    fn from(info: quote::ParticipantInfo) -> Self {
741        Self {
742            broker_ids: info.broker_ids,
743            name_cn: info.participant_name_cn,
744            name_en: info.participant_name_en,
745            name_hk: info.participant_name_hk,
746        }
747    }
748}
749
750/// Intraday line
751#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct IntradayLine {
753    /// Close price of the minute
754    pub price: Decimal,
755    /// Start time of the minute
756    #[serde(with = "time::serde::rfc3339")]
757    pub timestamp: OffsetDateTime,
758    /// Volume
759    pub volume: i64,
760    /// Turnover
761    pub turnover: Decimal,
762    /// Average price
763    pub avg_price: Decimal,
764}
765
766impl TryFrom<quote::Line> for IntradayLine {
767    type Error = Error;
768
769    fn try_from(value: quote::Line) -> Result<Self> {
770        Ok(Self {
771            price: value.price.parse().unwrap_or_default(),
772            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
773                .map_err(|err| Error::parse_field_error("timestamp", err))?,
774            volume: value.volume,
775            turnover: value.turnover.parse().unwrap_or_default(),
776            avg_price: value.avg_price.parse().unwrap_or_default(),
777        })
778    }
779}
780
781/// Candlestick
782#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
783pub struct Candlestick {
784    /// Close price
785    pub close: Decimal,
786    /// Open price
787    pub open: Decimal,
788    /// Low price
789    pub low: Decimal,
790    /// High price
791    pub high: Decimal,
792    /// Volume
793    pub volume: i64,
794    /// Turnover
795    pub turnover: Decimal,
796    /// Timestamp
797    #[serde(with = "time::serde::rfc3339")]
798    pub timestamp: OffsetDateTime,
799    /// Trade session
800    pub trade_session: TradeSession,
801    open_updated: bool,
802}
803
804impl TryFrom<quote::Candlestick> for Candlestick {
805    type Error = Error;
806
807    fn try_from(value: quote::Candlestick) -> Result<Self> {
808        Ok(Self {
809            close: value.close.parse().unwrap_or_default(),
810            open: value.open.parse().unwrap_or_default(),
811            low: value.low.parse().unwrap_or_default(),
812            high: value.high.parse().unwrap_or_default(),
813            volume: value.volume,
814            turnover: value.turnover.parse().unwrap_or_default(),
815            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
816                .map_err(|err| Error::parse_field_error("timestamp", err))?,
817            trade_session: longport_proto::quote::TradeSession::try_from(value.trade_session)
818                .map_err(|err| Error::parse_field_error("trade_session", err))?
819                .into(),
820            open_updated: true,
821        })
822    }
823}
824
825impl longport_candlesticks::CandlestickType for Candlestick {
826    type PriceType = Decimal;
827    type VolumeType = i64;
828    type TurnoverType = Decimal;
829    type TradeSessionType = TradeSession;
830
831    #[inline]
832    fn new(
833        components: CandlestickComponents<
834            Self::PriceType,
835            Self::VolumeType,
836            Self::TurnoverType,
837            Self::TradeSessionType,
838        >,
839    ) -> Self {
840        Self {
841            timestamp: components.time,
842            open: components.open,
843            high: components.high,
844            low: components.low,
845            close: components.close,
846            volume: components.volume,
847            turnover: components.turnover,
848            trade_session: components.trade_session,
849            open_updated: components.open_updated,
850        }
851    }
852
853    #[inline]
854    fn time(&self) -> OffsetDateTime {
855        self.timestamp
856    }
857
858    #[inline]
859    fn set_time(&mut self, time: OffsetDateTime) {
860        self.timestamp = time;
861    }
862
863    #[inline]
864    fn open(&self) -> Self::PriceType {
865        self.open
866    }
867
868    #[inline]
869    fn set_open(&mut self, open: Self::PriceType) {
870        self.open = open;
871    }
872
873    #[inline]
874    fn high(&self) -> Self::PriceType {
875        self.high
876    }
877
878    #[inline]
879    fn set_high(&mut self, high: Self::PriceType) {
880        self.high = high;
881    }
882
883    #[inline]
884    fn low(&self) -> Self::PriceType {
885        self.low
886    }
887
888    #[inline]
889    fn set_low(&mut self, low: Self::PriceType) {
890        self.low = low;
891    }
892
893    #[inline]
894    fn close(&self) -> Self::PriceType {
895        self.close
896    }
897
898    #[inline]
899    fn set_close(&mut self, close: Self::PriceType) {
900        self.close = close;
901    }
902
903    #[inline]
904    fn volume(&self) -> Self::VolumeType {
905        self.volume
906    }
907
908    #[inline]
909    fn set_volume(&mut self, volume: Self::VolumeType) {
910        self.volume = volume;
911    }
912
913    #[inline]
914    fn turnover(&self) -> Self::TurnoverType {
915        self.turnover
916    }
917
918    #[inline]
919    fn set_turnover(&mut self, turnover: Self::TurnoverType) {
920        self.turnover = turnover;
921    }
922
923    #[inline]
924    fn trade_session(&self) -> Self::TradeSessionType {
925        self.trade_session
926    }
927
928    #[inline]
929    fn set_open_updated(&mut self, updated: bool) {
930        self.open_updated = updated;
931    }
932
933    #[inline]
934    fn open_updated(&self) -> bool {
935        self.open_updated
936    }
937}
938
939/// Strike price info
940#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct StrikePriceInfo {
942    /// Strike price
943    pub price: Decimal,
944    /// Security code of call option
945    pub call_symbol: String,
946    /// Security code of put option
947    pub put_symbol: String,
948    /// Is standard
949    pub standard: bool,
950}
951
952impl TryFrom<quote::StrikePriceInfo> for StrikePriceInfo {
953    type Error = Error;
954
955    fn try_from(value: quote::StrikePriceInfo) -> Result<Self> {
956        Ok(Self {
957            price: value.price.parse().unwrap_or_default(),
958            call_symbol: value.call_symbol,
959            put_symbol: value.put_symbol,
960            standard: value.standard,
961        })
962    }
963}
964
965/// Issuer info
966#[derive(Debug, Clone, Serialize, Deserialize)]
967pub struct IssuerInfo {
968    /// Issuer ID
969    pub issuer_id: i32,
970    /// Issuer name (zh-CN)
971    pub name_cn: String,
972    /// Issuer name (en)
973    pub name_en: String,
974    /// Issuer name (zh-HK)
975    pub name_hk: String,
976}
977
978impl From<quote::IssuerInfo> for IssuerInfo {
979    fn from(info: quote::IssuerInfo) -> Self {
980        Self {
981            issuer_id: info.id,
982            name_cn: info.name_cn,
983            name_en: info.name_en,
984            name_hk: info.name_hk,
985        }
986    }
987}
988
989/// Sort order type
990#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
991#[repr(i32)]
992pub enum SortOrderType {
993    /// Ascending
994    Ascending = 0,
995    /// Descending
996    Descending = 1,
997}
998
999/// Warrant sort by
1000#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1001#[repr(i32)]
1002pub enum WarrantSortBy {
1003    /// Last done
1004    LastDone = 0,
1005    /// Change rate
1006    ChangeRate = 1,
1007    /// Change value
1008    ChangeValue = 2,
1009    /// Volume
1010    Volume = 3,
1011    /// Turnover
1012    Turnover = 4,
1013    /// Expiry date
1014    ExpiryDate = 5,
1015    /// Strike price
1016    StrikePrice = 6,
1017    /// Upper strike price
1018    UpperStrikePrice = 7,
1019    /// Lower strike price
1020    LowerStrikePrice = 8,
1021    /// Outstanding quantity
1022    OutstandingQuantity = 9,
1023    /// Outstanding ratio
1024    OutstandingRatio = 10,
1025    /// Premium
1026    Premium = 11,
1027    /// In/out of the bound
1028    ItmOtm = 12,
1029    /// Implied volatility
1030    ImpliedVolatility = 13,
1031    /// Greek value Delta
1032    Delta = 14,
1033    /// Call price
1034    CallPrice = 15,
1035    /// Price interval from the call price
1036    ToCallPrice = 16,
1037    /// Effective leverage
1038    EffectiveLeverage = 17,
1039    /// Leverage ratio
1040    LeverageRatio = 18,
1041    /// Conversion ratio
1042    ConversionRatio = 19,
1043    /// Breakeven point
1044    BalancePoint = 20,
1045    /// Status
1046    Status = 21,
1047}
1048
1049/// Filter warrant expiry date type
1050#[allow(non_camel_case_types)]
1051#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1052#[repr(i32)]
1053pub enum FilterWarrantExpiryDate {
1054    /// Less than 3 months
1055    LT_3 = 1,
1056    /// 3 - 6 months
1057    Between_3_6 = 2,
1058    /// 6 - 12 months
1059    Between_6_12 = 3,
1060    /// Greater than 12 months
1061    GT_12 = 4,
1062}
1063
1064/// Filter warrant in/out of the bounds type
1065#[allow(non_camel_case_types)]
1066#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1067#[repr(i32)]
1068pub enum FilterWarrantInOutBoundsType {
1069    /// In bounds
1070    In = 1,
1071    /// Out bounds
1072    Out = 2,
1073}
1074
1075/// Warrant status
1076#[derive(
1077    Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
1078)]
1079#[repr(i32)]
1080pub enum WarrantStatus {
1081    /// Suspend
1082    Suspend = 2,
1083    /// Prepare List
1084    PrepareList = 3,
1085    /// Normal
1086    Normal = 4,
1087}
1088
1089/// Warrant info
1090#[derive(Debug, Clone, Serialize, Deserialize)]
1091pub struct WarrantInfo {
1092    /// Security code
1093    pub symbol: String,
1094    /// Warrant type
1095    pub warrant_type: WarrantType,
1096    /// Security name
1097    pub name: String,
1098    /// Latest price
1099    pub last_done: Decimal,
1100    /// Quote change rate
1101    pub change_rate: Decimal,
1102    /// Quote change
1103    pub change_value: Decimal,
1104    /// Volume
1105    pub volume: i64,
1106    /// Turnover
1107    pub turnover: Decimal,
1108    /// Expiry date
1109    pub expiry_date: Date,
1110    /// Strike price
1111    pub strike_price: Option<Decimal>,
1112    /// Upper strike price
1113    pub upper_strike_price: Option<Decimal>,
1114    /// Lower strike price
1115    pub lower_strike_price: Option<Decimal>,
1116    /// Outstanding quantity
1117    pub outstanding_qty: i64,
1118    /// Outstanding ratio
1119    pub outstanding_ratio: Decimal,
1120    /// Premium
1121    pub premium: Decimal,
1122    /// In/out of the bound
1123    pub itm_otm: Option<Decimal>,
1124    /// Implied volatility
1125    pub implied_volatility: Option<Decimal>,
1126    /// Delta
1127    pub delta: Option<Decimal>,
1128    /// Call price
1129    pub call_price: Option<Decimal>,
1130    /// Price interval from the call price
1131    pub to_call_price: Option<Decimal>,
1132    /// Effective leverage
1133    pub effective_leverage: Option<Decimal>,
1134    /// Leverage ratio
1135    pub leverage_ratio: Decimal,
1136    /// Conversion ratio
1137    pub conversion_ratio: Option<Decimal>,
1138    /// Breakeven point
1139    pub balance_point: Option<Decimal>,
1140    /// Status
1141    pub status: WarrantStatus,
1142}
1143
1144impl TryFrom<quote::FilterWarrant> for WarrantInfo {
1145    type Error = Error;
1146
1147    fn try_from(info: quote::FilterWarrant) -> Result<Self> {
1148        let r#type = WarrantType::try_from(info.r#type)
1149            .map_err(|err| Error::parse_field_error("type", err))?;
1150
1151        match r#type {
1152            WarrantType::Unknown => unreachable!(),
1153            WarrantType::Call | WarrantType::Put => Ok(Self {
1154                symbol: info.symbol,
1155                warrant_type: r#type,
1156                name: info.name,
1157                last_done: info.last_done.parse().unwrap_or_default(),
1158                change_rate: info.change_rate.parse().unwrap_or_default(),
1159                change_value: info.change_val.parse().unwrap_or_default(),
1160                volume: info.volume,
1161                turnover: info.turnover.parse().unwrap_or_default(),
1162                expiry_date: parse_date(&info.expiry_date)
1163                    .map_err(|err| Error::parse_field_error("expiry_date", err))?,
1164                strike_price: Some(info.last_done.parse().unwrap_or_default()),
1165                upper_strike_price: None,
1166                lower_strike_price: None,
1167                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1168                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1169                premium: info.premium.parse().unwrap_or_default(),
1170                itm_otm: Some(info.last_done.parse().unwrap_or_default()),
1171                implied_volatility: Some(info.last_done.parse().unwrap_or_default()),
1172                delta: Some(info.last_done.parse().unwrap_or_default()),
1173                call_price: None,
1174                to_call_price: None,
1175                effective_leverage: Some(info.last_done.parse().unwrap_or_default()),
1176                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1177                conversion_ratio: Some(info.last_done.parse().unwrap_or_default()),
1178                balance_point: Some(info.last_done.parse().unwrap_or_default()),
1179                status: WarrantStatus::try_from(info.status)
1180                    .map_err(|err| Error::parse_field_error("state", err))?,
1181            }),
1182            WarrantType::Bull | WarrantType::Bear => Ok(Self {
1183                symbol: info.symbol,
1184                warrant_type: r#type,
1185                name: info.name,
1186                last_done: info.last_done.parse().unwrap_or_default(),
1187                change_rate: info.change_rate.parse().unwrap_or_default(),
1188                change_value: info.change_val.parse().unwrap_or_default(),
1189                volume: info.volume,
1190                turnover: info.turnover.parse().unwrap_or_default(),
1191                expiry_date: parse_date(&info.expiry_date)
1192                    .map_err(|err| Error::parse_field_error("expiry_date", err))?,
1193                strike_price: Some(info.last_done.parse().unwrap_or_default()),
1194                upper_strike_price: None,
1195                lower_strike_price: None,
1196                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1197                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1198                premium: info.premium.parse().unwrap_or_default(),
1199                itm_otm: Some(info.last_done.parse().unwrap_or_default()),
1200                implied_volatility: None,
1201                delta: None,
1202                call_price: Some(info.call_price.parse().unwrap_or_default()),
1203                to_call_price: Some(info.to_call_price.parse().unwrap_or_default()),
1204                effective_leverage: None,
1205                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1206                conversion_ratio: Some(info.last_done.parse().unwrap_or_default()),
1207                balance_point: Some(info.last_done.parse().unwrap_or_default()),
1208                status: WarrantStatus::try_from(info.status)
1209                    .map_err(|err| Error::parse_field_error("state", err))?,
1210            }),
1211            WarrantType::Inline => Ok(Self {
1212                symbol: info.symbol,
1213                warrant_type: r#type,
1214                name: info.name,
1215                last_done: info.last_done.parse().unwrap_or_default(),
1216                change_rate: info.change_rate.parse().unwrap_or_default(),
1217                change_value: info.change_val.parse().unwrap_or_default(),
1218                volume: info.volume,
1219                turnover: info.turnover.parse().unwrap_or_default(),
1220                expiry_date: parse_date(&info.expiry_date)
1221                    .map_err(|err| Error::parse_field_error("expiry_date", err))?,
1222                strike_price: None,
1223                upper_strike_price: Some(info.upper_strike_price.parse().unwrap_or_default()),
1224                lower_strike_price: Some(info.lower_strike_price.parse().unwrap_or_default()),
1225                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1226                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1227                premium: info.premium.parse().unwrap_or_default(),
1228                itm_otm: None,
1229                implied_volatility: None,
1230                delta: None,
1231                call_price: None,
1232                to_call_price: None,
1233                effective_leverage: None,
1234                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1235                conversion_ratio: None,
1236                balance_point: None,
1237                status: WarrantStatus::try_from(info.status)
1238                    .map_err(|err| Error::parse_field_error("state", err))?,
1239            }),
1240        }
1241    }
1242}
1243
1244/// The information of trading session
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub struct TradingSessionInfo {
1247    /// Being trading time
1248    pub begin_time: Time,
1249    /// End trading time
1250    pub end_time: Time,
1251    /// Trading session
1252    pub trade_session: TradeSession,
1253}
1254
1255impl TryFrom<quote::TradePeriod> for TradingSessionInfo {
1256    type Error = Error;
1257
1258    fn try_from(value: quote::TradePeriod) -> Result<Self> {
1259        #[inline]
1260        fn parse_time(value: i32) -> ::std::result::Result<Time, time::error::ComponentRange> {
1261            Time::from_hms(((value / 100) % 100) as u8, (value % 100) as u8, 0)
1262        }
1263
1264        Ok(Self {
1265            begin_time: parse_time(value.beg_time)
1266                .map_err(|err| Error::parse_field_error("beg_time", err))?,
1267            end_time: parse_time(value.end_time)
1268                .map_err(|err| Error::parse_field_error("end_time", err))?,
1269            trade_session: longport_proto::quote::TradeSession::try_from(value.trade_session)
1270                .unwrap_or_default()
1271                .into(),
1272        })
1273    }
1274}
1275
1276/// Market trading session
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1278pub struct MarketTradingSession {
1279    /// Market
1280    pub market: Market,
1281    /// Trading session
1282    pub trade_sessions: Vec<TradingSessionInfo>,
1283}
1284
1285impl TryFrom<quote::MarketTradePeriod> for MarketTradingSession {
1286    type Error = Error;
1287
1288    fn try_from(value: quote::MarketTradePeriod) -> Result<Self> {
1289        Ok(Self {
1290            market: value.market.parse().unwrap_or_default(),
1291            trade_sessions: value
1292                .trade_session
1293                .into_iter()
1294                .map(TryInto::try_into)
1295                .collect::<Result<Vec<_>>>()?,
1296        })
1297    }
1298}
1299
1300/// Market trading days
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1302pub struct MarketTradingDays {
1303    /// Trading days
1304    pub trading_days: Vec<Date>,
1305    /// Half trading days
1306    pub half_trading_days: Vec<Date>,
1307}
1308
1309/// Capital flow line
1310#[derive(Debug, Clone, Serialize, Deserialize)]
1311pub struct CapitalFlowLine {
1312    /// Inflow capital data
1313    pub inflow: Decimal,
1314    /// Time
1315    pub timestamp: OffsetDateTime,
1316}
1317
1318impl TryFrom<quote::capital_flow_intraday_response::CapitalFlowLine> for CapitalFlowLine {
1319    type Error = Error;
1320
1321    fn try_from(value: quote::capital_flow_intraday_response::CapitalFlowLine) -> Result<Self> {
1322        Ok(Self {
1323            inflow: value.inflow.parse().unwrap_or_default(),
1324            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
1325                .map_err(|err| Error::parse_field_error("timestamp", err))?,
1326        })
1327    }
1328}
1329
1330/// Capital distribution
1331#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1332pub struct CapitalDistribution {
1333    /// Large order
1334    pub large: Decimal,
1335    /// Medium order
1336    pub medium: Decimal,
1337    /// Small order
1338    pub small: Decimal,
1339}
1340
1341impl TryFrom<quote::capital_distribution_response::CapitalDistribution> for CapitalDistribution {
1342    type Error = Error;
1343
1344    fn try_from(value: quote::capital_distribution_response::CapitalDistribution) -> Result<Self> {
1345        Ok(Self {
1346            large: value.large.parse().unwrap_or_default(),
1347            medium: value.medium.parse().unwrap_or_default(),
1348            small: value.small.parse().unwrap_or_default(),
1349        })
1350    }
1351}
1352
1353/// Capital distribution response
1354#[derive(Debug, Clone, Serialize, Deserialize)]
1355pub struct CapitalDistributionResponse {
1356    /// Time
1357    pub timestamp: OffsetDateTime,
1358    /// Inflow capital data
1359    pub capital_in: CapitalDistribution,
1360    /// Outflow capital data
1361    pub capital_out: CapitalDistribution,
1362}
1363
1364impl TryFrom<quote::CapitalDistributionResponse> for CapitalDistributionResponse {
1365    type Error = Error;
1366
1367    fn try_from(value: quote::CapitalDistributionResponse) -> Result<Self> {
1368        Ok(Self {
1369            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
1370                .map_err(|err| Error::parse_field_error("timestamp", err))?,
1371            capital_in: value
1372                .capital_in
1373                .map(TryInto::try_into)
1374                .transpose()?
1375                .unwrap_or_default(),
1376            capital_out: value
1377                .capital_out
1378                .map(TryInto::try_into)
1379                .transpose()?
1380                .unwrap_or_default(),
1381        })
1382    }
1383}
1384
1385/// Watchlist security
1386#[derive(Debug, Clone, Serialize, Deserialize)]
1387pub struct WatchlistSecurity {
1388    /// Security symbol
1389    pub symbol: String,
1390    /// Market
1391    pub market: Market,
1392    /// Security name
1393    pub name: String,
1394    /// Watched price
1395    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
1396    pub watched_price: Option<Decimal>,
1397    /// Watched time
1398    #[serde(
1399        serialize_with = "time::serde::rfc3339::serialize",
1400        deserialize_with = "serde_utils::timestamp::deserialize"
1401    )]
1402    pub watched_at: OffsetDateTime,
1403}
1404
1405/// Watchlist group
1406#[derive(Debug, Clone, Serialize, Deserialize)]
1407pub struct WatchlistGroup {
1408    /// Group id
1409    #[serde(with = "serde_utils::int64_str")]
1410    pub id: i64,
1411    /// Group name
1412    pub name: String,
1413    /// Securities
1414    pub securities: Vec<WatchlistSecurity>,
1415}
1416
1417/// An request for create watchlist group
1418#[derive(Debug, Clone)]
1419pub struct RequestCreateWatchlistGroup {
1420    /// Group name
1421    pub name: String,
1422    /// Securities
1423    pub securities: Option<Vec<String>>,
1424}
1425
1426impl RequestCreateWatchlistGroup {
1427    /// Create a new request for create watchlist group
1428    pub fn new(name: impl Into<String>) -> Self {
1429        Self {
1430            name: name.into(),
1431            securities: None,
1432        }
1433    }
1434
1435    /// Set securities to the request
1436    pub fn securities<I, T>(self, securities: I) -> Self
1437    where
1438        I: IntoIterator<Item = T>,
1439        T: Into<String>,
1440    {
1441        Self {
1442            securities: Some(securities.into_iter().map(Into::into).collect()),
1443            ..self
1444        }
1445    }
1446}
1447
1448/// Securities update mode
1449#[derive(Debug, Copy, Clone, Default, Serialize)]
1450#[serde(rename_all = "lowercase")]
1451pub enum SecuritiesUpdateMode {
1452    /// Add securities
1453    Add,
1454    /// Remove securities
1455    Remove,
1456    /// Replace securities
1457    #[default]
1458    Replace,
1459}
1460
1461/// An request for update watchlist group
1462#[derive(Debug, Clone)]
1463pub struct RequestUpdateWatchlistGroup {
1464    /// Group id
1465    pub id: i64,
1466    /// Group name
1467    pub name: Option<String>,
1468    /// Securities
1469    pub securities: Option<Vec<String>>,
1470    /// Securities Update mode
1471    pub mode: SecuritiesUpdateMode,
1472}
1473
1474impl RequestUpdateWatchlistGroup {
1475    /// Create a new request for update watchlist group
1476    #[inline]
1477    pub fn new(id: i64) -> Self {
1478        Self {
1479            id,
1480            name: None,
1481            securities: None,
1482            mode: SecuritiesUpdateMode::default(),
1483        }
1484    }
1485
1486    /// Set group name to the request
1487    pub fn name(self, name: impl Into<String>) -> Self {
1488        Self {
1489            name: Some(name.into()),
1490            ..self
1491        }
1492    }
1493
1494    /// Set securities to the request
1495    pub fn securities<I, T>(self, securities: I) -> Self
1496    where
1497        I: IntoIterator<Item = T>,
1498        T: Into<String>,
1499    {
1500        Self {
1501            securities: Some(securities.into_iter().map(Into::into).collect()),
1502            ..self
1503        }
1504    }
1505
1506    /// Set securities update mode to the request
1507    pub fn mode(self, mode: SecuritiesUpdateMode) -> Self {
1508        Self { mode, ..self }
1509    }
1510}
1511
1512/// Calc index
1513#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1514pub enum CalcIndex {
1515    /// Latest price
1516    LastDone,
1517    /// Change value
1518    ChangeValue,
1519    /// Change rate
1520    ChangeRate,
1521    /// Volume
1522    Volume,
1523    /// Turnover
1524    Turnover,
1525    /// Year-to-date change ratio
1526    YtdChangeRate,
1527    /// Turnover rate
1528    TurnoverRate,
1529    /// Total market value
1530    TotalMarketValue,
1531    /// Capital flow
1532    CapitalFlow,
1533    /// Amplitude
1534    Amplitude,
1535    /// Volume ratio
1536    VolumeRatio,
1537    /// PE (TTM)
1538    PeTtmRatio,
1539    /// PB
1540    PbRatio,
1541    /// Dividend ratio (TTM)
1542    DividendRatioTtm,
1543    /// Five days change ratio
1544    FiveDayChangeRate,
1545    /// Ten days change ratio
1546    TenDayChangeRate,
1547    /// Half year change ratio
1548    HalfYearChangeRate,
1549    /// Five minutes change ratio
1550    FiveMinutesChangeRate,
1551    /// Expiry date
1552    ExpiryDate,
1553    /// Strike price
1554    StrikePrice,
1555    /// Upper bound price
1556    UpperStrikePrice,
1557    /// Lower bound price
1558    LowerStrikePrice,
1559    /// Outstanding quantity
1560    OutstandingQty,
1561    /// Outstanding ratio
1562    OutstandingRatio,
1563    /// Premium
1564    Premium,
1565    /// In/out of the bound
1566    ItmOtm,
1567    /// Implied volatility
1568    ImpliedVolatility,
1569    /// Warrant delta
1570    WarrantDelta,
1571    /// Call price
1572    CallPrice,
1573    /// Price interval from the call price
1574    ToCallPrice,
1575    /// Effective leverage
1576    EffectiveLeverage,
1577    /// Leverage ratio
1578    LeverageRatio,
1579    /// Conversion ratio
1580    ConversionRatio,
1581    /// Breakeven point
1582    BalancePoint,
1583    /// Open interest
1584    OpenInterest,
1585    /// Delta
1586    Delta,
1587    /// Gamma
1588    Gamma,
1589    /// Theta
1590    Theta,
1591    /// Vega
1592    Vega,
1593    /// Rho
1594    Rho,
1595}
1596
1597impl From<CalcIndex> for longport_proto::quote::CalcIndex {
1598    fn from(value: CalcIndex) -> Self {
1599        use longport_proto::quote::CalcIndex::*;
1600
1601        match value {
1602            CalcIndex::LastDone => CalcindexLastDone,
1603            CalcIndex::ChangeValue => CalcindexChangeVal,
1604            CalcIndex::ChangeRate => CalcindexChangeRate,
1605            CalcIndex::Volume => CalcindexVolume,
1606            CalcIndex::Turnover => CalcindexTurnover,
1607            CalcIndex::YtdChangeRate => CalcindexYtdChangeRate,
1608            CalcIndex::TurnoverRate => CalcindexTurnoverRate,
1609            CalcIndex::TotalMarketValue => CalcindexTotalMarketValue,
1610            CalcIndex::CapitalFlow => CalcindexCapitalFlow,
1611            CalcIndex::Amplitude => CalcindexAmplitude,
1612            CalcIndex::VolumeRatio => CalcindexVolumeRatio,
1613            CalcIndex::PeTtmRatio => CalcindexPeTtmRatio,
1614            CalcIndex::PbRatio => CalcindexPbRatio,
1615            CalcIndex::DividendRatioTtm => CalcindexDividendRatioTtm,
1616            CalcIndex::FiveDayChangeRate => CalcindexFiveDayChangeRate,
1617            CalcIndex::TenDayChangeRate => CalcindexTenDayChangeRate,
1618            CalcIndex::HalfYearChangeRate => CalcindexHalfYearChangeRate,
1619            CalcIndex::FiveMinutesChangeRate => CalcindexFiveMinutesChangeRate,
1620            CalcIndex::ExpiryDate => CalcindexExpiryDate,
1621            CalcIndex::StrikePrice => CalcindexStrikePrice,
1622            CalcIndex::UpperStrikePrice => CalcindexUpperStrikePrice,
1623            CalcIndex::LowerStrikePrice => CalcindexLowerStrikePrice,
1624            CalcIndex::OutstandingQty => CalcindexOutstandingQty,
1625            CalcIndex::OutstandingRatio => CalcindexOutstandingRatio,
1626            CalcIndex::Premium => CalcindexPremium,
1627            CalcIndex::ItmOtm => CalcindexItmOtm,
1628            CalcIndex::ImpliedVolatility => CalcindexImpliedVolatility,
1629            CalcIndex::WarrantDelta => CalcindexWarrantDelta,
1630            CalcIndex::CallPrice => CalcindexCallPrice,
1631            CalcIndex::ToCallPrice => CalcindexToCallPrice,
1632            CalcIndex::EffectiveLeverage => CalcindexEffectiveLeverage,
1633            CalcIndex::LeverageRatio => CalcindexLeverageRatio,
1634            CalcIndex::ConversionRatio => CalcindexConversionRatio,
1635            CalcIndex::BalancePoint => CalcindexBalancePoint,
1636            CalcIndex::OpenInterest => CalcindexOpenInterest,
1637            CalcIndex::Delta => CalcindexDelta,
1638            CalcIndex::Gamma => CalcindexGamma,
1639            CalcIndex::Theta => CalcindexTheta,
1640            CalcIndex::Vega => CalcindexVega,
1641            CalcIndex::Rho => CalcindexRho,
1642        }
1643    }
1644}
1645
1646/// Security calc index response
1647#[derive(Debug, Clone, Serialize, Deserialize)]
1648pub struct SecurityCalcIndex {
1649    /// Security code
1650    pub symbol: String,
1651    /// Latest price
1652    pub last_done: Option<Decimal>,
1653    /// Change value
1654    pub change_value: Option<Decimal>,
1655    /// Change ratio
1656    pub change_rate: Option<Decimal>,
1657    /// Volume
1658    pub volume: Option<i64>,
1659    /// Turnover
1660    pub turnover: Option<Decimal>,
1661    /// Year-to-date change ratio
1662    pub ytd_change_rate: Option<Decimal>,
1663    /// Turnover rate
1664    pub turnover_rate: Option<Decimal>,
1665    /// Total market value
1666    pub total_market_value: Option<Decimal>,
1667    /// Capital flow
1668    pub capital_flow: Option<Decimal>,
1669    /// Amplitude
1670    pub amplitude: Option<Decimal>,
1671    /// Volume ratio
1672    pub volume_ratio: Option<Decimal>,
1673    /// PE (TTM)
1674    pub pe_ttm_ratio: Option<Decimal>,
1675    /// PB
1676    pub pb_ratio: Option<Decimal>,
1677    /// Dividend ratio (TTM)
1678    pub dividend_ratio_ttm: Option<Decimal>,
1679    /// Five days change ratio
1680    pub five_day_change_rate: Option<Decimal>,
1681    /// Ten days change ratio
1682    pub ten_day_change_rate: Option<Decimal>,
1683    /// Half year change ratio
1684    pub half_year_change_rate: Option<Decimal>,
1685    /// Five minutes change ratio
1686    pub five_minutes_change_rate: Option<Decimal>,
1687    /// Expiry date
1688    pub expiry_date: Option<Date>,
1689    /// Strike price
1690    pub strike_price: Option<Decimal>,
1691    /// Upper bound price
1692    pub upper_strike_price: Option<Decimal>,
1693    /// Lower bound price
1694    pub lower_strike_price: Option<Decimal>,
1695    /// Outstanding quantity
1696    pub outstanding_qty: Option<i64>,
1697    /// Outstanding ratio
1698    pub outstanding_ratio: Option<Decimal>,
1699    /// Premium
1700    pub premium: Option<Decimal>,
1701    /// In/out of the bound
1702    pub itm_otm: Option<Decimal>,
1703    /// Implied volatility
1704    pub implied_volatility: Option<Decimal>,
1705    /// Warrant delta
1706    pub warrant_delta: Option<Decimal>,
1707    /// Call price
1708    pub call_price: Option<Decimal>,
1709    /// Price interval from the call price
1710    pub to_call_price: Option<Decimal>,
1711    /// Effective leverage
1712    pub effective_leverage: Option<Decimal>,
1713    /// Leverage ratio
1714    pub leverage_ratio: Option<Decimal>,
1715    /// Conversion ratio
1716    pub conversion_ratio: Option<Decimal>,
1717    /// Breakeven point
1718    pub balance_point: Option<Decimal>,
1719    /// Open interest
1720    pub open_interest: Option<i64>,
1721    /// Delta
1722    pub delta: Option<Decimal>,
1723    /// Gamma
1724    pub gamma: Option<Decimal>,
1725    /// Theta
1726    pub theta: Option<Decimal>,
1727    /// Vega
1728    pub vega: Option<Decimal>,
1729    /// Rho
1730    pub rho: Option<Decimal>,
1731}
1732
1733impl SecurityCalcIndex {
1734    pub(crate) fn from_proto(
1735        resp: longport_proto::quote::SecurityCalcIndex,
1736        indexes: &[CalcIndex],
1737    ) -> Self {
1738        let mut output = SecurityCalcIndex {
1739            symbol: resp.symbol,
1740            last_done: None,
1741            change_value: None,
1742            change_rate: None,
1743            volume: None,
1744            turnover: None,
1745            ytd_change_rate: None,
1746            turnover_rate: None,
1747            total_market_value: None,
1748            capital_flow: None,
1749            amplitude: None,
1750            volume_ratio: None,
1751            pe_ttm_ratio: None,
1752            pb_ratio: None,
1753            dividend_ratio_ttm: None,
1754            five_day_change_rate: None,
1755            ten_day_change_rate: None,
1756            half_year_change_rate: None,
1757            five_minutes_change_rate: None,
1758            expiry_date: None,
1759            strike_price: None,
1760            upper_strike_price: None,
1761            lower_strike_price: None,
1762            outstanding_qty: None,
1763            outstanding_ratio: None,
1764            premium: None,
1765            itm_otm: None,
1766            implied_volatility: None,
1767            warrant_delta: None,
1768            call_price: None,
1769            to_call_price: None,
1770            effective_leverage: None,
1771            leverage_ratio: None,
1772            conversion_ratio: None,
1773            balance_point: None,
1774            open_interest: None,
1775            delta: None,
1776            gamma: None,
1777            theta: None,
1778            vega: None,
1779            rho: None,
1780        };
1781
1782        for index in indexes {
1783            match index {
1784                CalcIndex::LastDone => output.last_done = resp.last_done.parse().ok(),
1785                CalcIndex::ChangeValue => output.change_value = resp.change_val.parse().ok(),
1786                CalcIndex::ChangeRate => output.change_rate = resp.change_rate.parse().ok(),
1787                CalcIndex::Volume => output.volume = Some(resp.volume),
1788                CalcIndex::Turnover => output.turnover = resp.turnover.parse().ok(),
1789                CalcIndex::YtdChangeRate => {
1790                    output.ytd_change_rate = resp.ytd_change_rate.parse().ok()
1791                }
1792                CalcIndex::TurnoverRate => output.turnover_rate = resp.turnover_rate.parse().ok(),
1793                CalcIndex::TotalMarketValue => {
1794                    output.total_market_value = resp.total_market_value.parse().ok()
1795                }
1796                CalcIndex::CapitalFlow => output.capital_flow = resp.capital_flow.parse().ok(),
1797                CalcIndex::Amplitude => output.amplitude = resp.amplitude.parse().ok(),
1798                CalcIndex::VolumeRatio => output.volume_ratio = resp.volume_ratio.parse().ok(),
1799                CalcIndex::PeTtmRatio => output.pe_ttm_ratio = resp.pe_ttm_ratio.parse().ok(),
1800                CalcIndex::PbRatio => output.pb_ratio = resp.pb_ratio.parse().ok(),
1801                CalcIndex::DividendRatioTtm => {
1802                    output.dividend_ratio_ttm = resp.dividend_ratio_ttm.parse().ok()
1803                }
1804                CalcIndex::FiveDayChangeRate => {
1805                    output.five_day_change_rate = resp.five_day_change_rate.parse().ok()
1806                }
1807                CalcIndex::TenDayChangeRate => {
1808                    output.ten_day_change_rate = resp.ten_day_change_rate.parse().ok()
1809                }
1810                CalcIndex::HalfYearChangeRate => {
1811                    output.half_year_change_rate = resp.half_year_change_rate.parse().ok()
1812                }
1813                CalcIndex::FiveMinutesChangeRate => {
1814                    output.five_minutes_change_rate = resp.five_minutes_change_rate.parse().ok()
1815                }
1816                CalcIndex::ExpiryDate => output.expiry_date = parse_date(&resp.expiry_date).ok(),
1817                CalcIndex::StrikePrice => output.strike_price = resp.strike_price.parse().ok(),
1818                CalcIndex::UpperStrikePrice => {
1819                    output.upper_strike_price = resp.upper_strike_price.parse().ok()
1820                }
1821                CalcIndex::LowerStrikePrice => {
1822                    output.lower_strike_price = resp.lower_strike_price.parse().ok()
1823                }
1824                CalcIndex::OutstandingQty => output.outstanding_qty = Some(resp.outstanding_qty),
1825                CalcIndex::OutstandingRatio => {
1826                    output.outstanding_ratio = resp.outstanding_ratio.parse().ok()
1827                }
1828                CalcIndex::Premium => output.premium = resp.premium.parse().ok(),
1829                CalcIndex::ItmOtm => output.itm_otm = resp.itm_otm.parse().ok(),
1830                CalcIndex::ImpliedVolatility => {
1831                    output.implied_volatility = resp.implied_volatility.parse().ok()
1832                }
1833                CalcIndex::WarrantDelta => output.warrant_delta = resp.warrant_delta.parse().ok(),
1834                CalcIndex::CallPrice => output.call_price = resp.call_price.parse().ok(),
1835                CalcIndex::ToCallPrice => output.to_call_price = resp.to_call_price.parse().ok(),
1836                CalcIndex::EffectiveLeverage => {
1837                    output.effective_leverage = resp.effective_leverage.parse().ok()
1838                }
1839                CalcIndex::LeverageRatio => {
1840                    output.leverage_ratio = resp.leverage_ratio.parse().ok()
1841                }
1842                CalcIndex::ConversionRatio => {
1843                    output.conversion_ratio = resp.conversion_ratio.parse().ok()
1844                }
1845                CalcIndex::BalancePoint => output.balance_point = resp.balance_point.parse().ok(),
1846                CalcIndex::OpenInterest => output.open_interest = Some(resp.open_interest),
1847                CalcIndex::Delta => output.delta = resp.delta.parse().ok(),
1848                CalcIndex::Gamma => output.gamma = resp.gamma.parse().ok(),
1849                CalcIndex::Theta => output.theta = resp.theta.parse().ok(),
1850                CalcIndex::Vega => output.vega = resp.vega.parse().ok(),
1851                CalcIndex::Rho => output.rho = resp.rho.parse().ok(),
1852            }
1853        }
1854
1855        output
1856    }
1857}
1858
1859/// Security list category
1860#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1861pub enum SecurityListCategory {
1862    /// Overnight
1863    Overnight,
1864}
1865
1866impl_serialize_for_enum_string!(SecurityListCategory);
1867
1868/// The basic information of securities
1869#[derive(Debug, Serialize, Deserialize)]
1870pub struct Security {
1871    /// Security code
1872    pub symbol: String,
1873    /// Security name (zh-CN)
1874    pub name_cn: String,
1875    /// Security name (en)
1876    pub name_en: String,
1877    /// Security name (zh-HK)
1878    pub name_hk: String,
1879}
1880
1881/// Quote package detail
1882#[derive(Debug, Clone)]
1883pub struct QuotePackageDetail {
1884    /// Key
1885    pub key: String,
1886    /// Name
1887    pub name: String,
1888    /// Description
1889    pub description: String,
1890    /// Start time
1891    pub start_at: OffsetDateTime,
1892    /// End time
1893    pub end_at: OffsetDateTime,
1894}
1895
1896impl TryFrom<quote::user_quote_level_detail::PackageDetail> for QuotePackageDetail {
1897    type Error = Error;
1898
1899    fn try_from(quote: quote::user_quote_level_detail::PackageDetail) -> Result<Self> {
1900        Ok(Self {
1901            key: quote.key,
1902            name: quote.name,
1903            description: quote.description,
1904            start_at: OffsetDateTime::from_unix_timestamp(quote.start)
1905                .map_err(|err| Error::parse_field_error("start_at", err))?,
1906            end_at: OffsetDateTime::from_unix_timestamp(quote.end)
1907                .map_err(|err| Error::parse_field_error("end_at", err))?,
1908        })
1909    }
1910}
1911
1912/// Trade sessions
1913#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1914#[repr(i32)]
1915pub enum TradeSessions {
1916    /// Intraday
1917    Intraday = 0,
1918    /// All
1919    All = 100,
1920}
1921
1922impl TradeSessions {
1923    #[inline]
1924    pub(crate) fn contains(&self, session: TradeSession) -> bool {
1925        match self {
1926            TradeSessions::Intraday => session == TradeSession::Intraday,
1927            TradeSessions::All => true,
1928        }
1929    }
1930}
1931
1932/// Market temperature
1933#[derive(Debug, Clone, Serialize, Deserialize)]
1934pub struct MarketTemperature {
1935    /// Temperature value
1936    pub temperature: i32,
1937    /// Temperature description
1938    #[serde(default)]
1939    pub description: String,
1940    /// Market valuation
1941    pub valuation: i32,
1942    /// Market sentiment
1943    pub sentiment: i32,
1944    /// Time
1945    #[serde(
1946        serialize_with = "time::serde::rfc3339::serialize",
1947        deserialize_with = "serde_utils::timestamp::deserialize",
1948        alias = "updated_at"
1949    )]
1950    pub timestamp: OffsetDateTime,
1951}
1952
1953/// Data granularity
1954#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1955pub enum Granularity {
1956    /// Unknown
1957    Unknown,
1958    /// Daily
1959    #[strum(serialize = "daily")]
1960    Daily,
1961    /// Weekly
1962    #[strum(serialize = "weekly")]
1963    Weekly,
1964    /// Monthly
1965    #[strum(serialize = "monthly")]
1966    Monthly,
1967}
1968
1969/// History market temperature response
1970#[derive(Debug, Clone, Serialize, Deserialize)]
1971pub struct HistoryMarketTemperatureResponse {
1972    /// Granularity
1973    #[serde(rename = "type")]
1974    pub granularity: Granularity,
1975    /// Records
1976    #[serde(rename = "list")]
1977    pub records: Vec<MarketTemperature>,
1978}
1979
1980impl_serde_for_enum_string!(Granularity);
1981impl_default_for_enum_string!(
1982    OptionType,
1983    OptionDirection,
1984    WarrantType,
1985    SecurityBoard,
1986    Granularity
1987);
1988
1989#[cfg(test)]
1990mod tests {
1991    use serde::Deserialize;
1992
1993    use crate::{Market, quote::WatchlistGroup};
1994
1995    #[test]
1996    fn watch_list() {
1997        #[derive(Debug, Deserialize)]
1998        struct Response {
1999            groups: Vec<WatchlistGroup>,
2000        }
2001
2002        let json = r#"
2003        {
2004            "groups": [
2005                {
2006                    "id": "1",
2007                    "name": "Test",
2008                    "securities": [
2009                        {
2010                            "symbol": "AAPL",
2011                            "market": "US",
2012                            "name": "Apple Inc.",
2013                            "watched_price": "150.0",
2014                            "watched_at": "1633036800"
2015                        }
2016                    ]
2017                }
2018            ]
2019        }
2020        "#;
2021
2022        let response: Response = serde_json::from_str(json).unwrap();
2023        assert_eq!(response.groups.len(), 1);
2024        assert_eq!(response.groups[0].id, 1);
2025        assert_eq!(response.groups[0].name, "Test");
2026        assert_eq!(response.groups[0].securities.len(), 1);
2027        assert_eq!(response.groups[0].securities[0].symbol, "AAPL");
2028        assert_eq!(response.groups[0].securities[0].market, Market::US);
2029        assert_eq!(response.groups[0].securities[0].name, "Apple Inc.");
2030        assert_eq!(
2031            response.groups[0].securities[0].watched_price,
2032            Some(decimal!(150.0))
2033        );
2034        assert_eq!(
2035            response.groups[0].securities[0].watched_at.unix_timestamp(),
2036            1633036800
2037        );
2038    }
2039}