longport/quote/
types.rs

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