Skip to main content

longbridge/quote/
types.rs

1use longbridge_candlesticks::CandlestickComponents;
2use longbridge_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 longbridge_candlesticks::TradeSessionType for TradeSession {
30    #[inline]
31    fn kind(&self) -> longbridge_candlesticks::TradeSessionKind {
32        match self {
33            TradeSession::Intraday => longbridge_candlesticks::TRADE_SESSION_INTRADAY,
34            TradeSession::Pre => longbridge_candlesticks::TRADE_SESSION_PRE,
35            TradeSession::Post => longbridge_candlesticks::TRADE_SESSION_POST,
36            TradeSession::Overnight => longbridge_candlesticks::TRADE_SESSION_OVERNIGHT,
37        }
38    }
39}
40
41impl From<longbridge_proto::quote::TradeSession> for TradeSession {
42    #[inline]
43    fn from(value: longbridge_proto::quote::TradeSession) -> Self {
44        match value {
45            longbridge_proto::quote::TradeSession::NormalTrade => Self::Intraday,
46            longbridge_proto::quote::TradeSession::PreTrade => Self::Pre,
47            longbridge_proto::quote::TradeSession::PostTrade => Self::Post,
48            longbridge_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: longbridge_proto::quote::TradeSession::try_from(trade.trade_session)
182                .unwrap_or_default()
183                .into(),
184        })
185    }
186}
187
188impl longbridge_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    /// S&P 500 Index
287    SPXIndex,
288    /// CBOE Volatility Index
289    VIXIndex,
290}
291
292/// The basic information of securities
293#[derive(Debug, Serialize, Deserialize)]
294pub struct SecurityStaticInfo {
295    /// Security code
296    pub symbol: String,
297    /// Security name (zh-CN)
298    pub name_cn: String,
299    /// Security name (en)
300    pub name_en: String,
301    /// Security name (zh-HK)
302    pub name_hk: String,
303    /// Exchange which the security belongs to
304    pub exchange: String,
305    /// Trading currency
306    pub currency: String,
307    /// Lot size
308    pub lot_size: i32,
309    /// Total shares
310    pub total_shares: i64,
311    /// Circulating shares
312    pub circulating_shares: i64,
313    /// HK shares (only HK stocks)
314    pub hk_shares: i64,
315    /// Earnings per share
316    pub eps: Decimal,
317    /// Earnings per share (TTM)
318    pub eps_ttm: Decimal,
319    /// Net assets per share
320    pub bps: Decimal,
321    /// Dividend (per share), **not** the dividend yield (ratio).
322    pub dividend_yield: Decimal,
323    /// Types of supported derivatives
324    pub stock_derivatives: DerivativeType,
325    /// Board
326    pub board: SecurityBoard,
327}
328
329impl TryFrom<quote::StaticInfo> for SecurityStaticInfo {
330    type Error = Error;
331
332    fn try_from(resp: quote::StaticInfo) -> Result<Self> {
333        Ok(SecurityStaticInfo {
334            symbol: resp.symbol,
335            name_cn: resp.name_cn,
336            name_en: resp.name_en,
337            name_hk: resp.name_hk,
338            exchange: resp.exchange,
339            currency: resp.currency,
340            lot_size: resp.lot_size,
341            total_shares: resp.total_shares,
342            circulating_shares: resp.circulating_shares,
343            hk_shares: resp.hk_shares,
344            eps: resp.eps.parse().unwrap_or_default(),
345            eps_ttm: resp.eps_ttm.parse().unwrap_or_default(),
346            bps: resp.bps.parse().unwrap_or_default(),
347            dividend_yield: resp.dividend_yield.parse().unwrap_or_default(),
348            stock_derivatives: resp.stock_derivatives.into_iter().fold(
349                DerivativeType::empty(),
350                |acc, value| match value {
351                    1 => acc | DerivativeType::OPTION,
352                    2 => acc | DerivativeType::WARRANT,
353                    _ => acc,
354                },
355            ),
356            board: resp.board.parse().unwrap_or(SecurityBoard::Unknown),
357        })
358    }
359}
360
361/// Real-time quote
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct RealtimeQuote {
364    /// Security code
365    pub symbol: String,
366    /// Latest price
367    pub last_done: Decimal,
368    /// Open
369    pub open: Decimal,
370    /// High
371    pub high: Decimal,
372    /// Low
373    pub low: Decimal,
374    /// Time of latest price
375    pub timestamp: OffsetDateTime,
376    /// Volume
377    pub volume: i64,
378    /// Turnover
379    pub turnover: Decimal,
380    /// Security trading status
381    pub trade_status: TradeStatus,
382}
383
384/// Quote of US pre/post market
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct PrePostQuote {
387    /// Latest price
388    pub last_done: Decimal,
389    /// Time of latest price
390    #[serde(with = "time::serde::rfc3339")]
391    pub timestamp: OffsetDateTime,
392    /// Volume
393    pub volume: i64,
394    /// Turnover
395    pub turnover: Decimal,
396    /// High
397    pub high: Decimal,
398    /// Low
399    pub low: Decimal,
400    /// Close of the last trade session
401    pub prev_close: Decimal,
402}
403
404impl TryFrom<quote::PrePostQuote> for PrePostQuote {
405    type Error = Error;
406
407    fn try_from(quote: quote::PrePostQuote) -> Result<Self> {
408        Ok(Self {
409            last_done: quote.last_done.parse().unwrap_or_default(),
410            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
411                .map_err(|err| Error::parse_field_error("timestamp", err))?,
412            volume: quote.volume,
413            turnover: quote.turnover.parse().unwrap_or_default(),
414            high: quote.high.parse().unwrap_or_default(),
415            low: quote.low.parse().unwrap_or_default(),
416            prev_close: quote.prev_close.parse().unwrap_or_default(),
417        })
418    }
419}
420
421/// Quote of securitity
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct SecurityQuote {
424    /// Security code
425    pub symbol: String,
426    /// Latest price
427    pub last_done: Decimal,
428    /// Yesterday's close
429    pub prev_close: Decimal,
430    /// Open
431    pub open: Decimal,
432    /// High
433    pub high: Decimal,
434    /// Low
435    pub low: Decimal,
436    /// Time of latest price
437    #[serde(with = "time::serde::rfc3339")]
438    pub timestamp: OffsetDateTime,
439    /// Volume
440    pub volume: i64,
441    /// Turnover
442    pub turnover: Decimal,
443    /// Security trading status
444    pub trade_status: TradeStatus,
445    /// Quote of US pre market
446    pub pre_market_quote: Option<PrePostQuote>,
447    /// Quote of US post market
448    pub post_market_quote: Option<PrePostQuote>,
449    /// Quote of US overnight market
450    pub overnight_quote: Option<PrePostQuote>,
451}
452
453impl TryFrom<quote::SecurityQuote> for SecurityQuote {
454    type Error = Error;
455
456    fn try_from(quote: quote::SecurityQuote) -> Result<Self> {
457        Ok(Self {
458            symbol: quote.symbol,
459            last_done: quote.last_done.parse().unwrap_or_default(),
460            prev_close: quote.prev_close.parse().unwrap_or_default(),
461            open: quote.open.parse().unwrap_or_default(),
462            high: quote.high.parse().unwrap_or_default(),
463            low: quote.low.parse().unwrap_or_default(),
464            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
465                .map_err(|err| Error::parse_field_error("timestamp", err))?,
466            volume: quote.volume,
467            turnover: quote.turnover.parse().unwrap_or_default(),
468            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
469            pre_market_quote: quote.pre_market_quote.map(TryInto::try_into).transpose()?,
470            post_market_quote: quote.post_market_quote.map(TryInto::try_into).transpose()?,
471            overnight_quote: quote.over_night_quote.map(TryInto::try_into).transpose()?,
472        })
473    }
474}
475
476/// Option type
477#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
478pub enum OptionType {
479    /// Unknown
480    Unknown,
481    /// American
482    #[strum(serialize = "A")]
483    American,
484    /// Europe
485    #[strum(serialize = "U")]
486    Europe,
487}
488
489/// Option direction
490#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
491pub enum OptionDirection {
492    /// Unknown
493    Unknown,
494    /// Put
495    #[strum(serialize = "P")]
496    Put,
497    /// Call
498    #[strum(serialize = "C")]
499    Call,
500}
501
502/// Special expiration cycle of an option contract
503#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
504pub enum OptionExpiryCycleType {
505    /// Unknown
506    Unknown,
507    /// Standard monthly option
508    #[strum(serialize = "")]
509    Monthly,
510    /// Weekly option, expires weekly
511    #[strum(serialize = "W")]
512    Weekly,
513    /// Quarterly option, expires quarterly
514    #[strum(serialize = "Q")]
515    Quarterly,
516}
517
518/// Whether an option contract is a legacy contract left over from a corporate
519/// action (e.g. a stock split or a merger)
520#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
521pub enum OptionStandardAttr {
522    /// Unknown
523    Unknown,
524    /// A normal, active contract
525    #[strum(serialize = "")]
526    Normal,
527    /// A legacy contract produced by a corporate action
528    #[strum(serialize = "old")]
529    Old,
530}
531
532/// Quote of option
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct OptionQuote {
535    /// Security code
536    pub symbol: String,
537    /// Latest price
538    pub last_done: Decimal,
539    /// Yesterday's close
540    pub prev_close: Decimal,
541    /// Open
542    pub open: Decimal,
543    /// High
544    pub high: Decimal,
545    /// Low
546    pub low: Decimal,
547    /// Time of latest price
548    #[serde(with = "time::serde::rfc3339")]
549    pub timestamp: OffsetDateTime,
550    /// Volume
551    pub volume: i64,
552    /// Turnover
553    pub turnover: Decimal,
554    /// Security trading status
555    pub trade_status: TradeStatus,
556    /// Implied volatility
557    pub implied_volatility: Decimal,
558    /// Number of open positions
559    pub open_interest: i64,
560    /// Exprity date
561    pub expiry_date: Date,
562    /// Strike price
563    pub strike_price: Decimal,
564    /// Contract multiplier
565    pub contract_multiplier: Decimal,
566    /// Option type
567    pub contract_type: OptionType,
568    /// Contract size
569    pub contract_size: Decimal,
570    /// Option direction
571    pub direction: OptionDirection,
572    /// Underlying security historical volatility of the option
573    pub historical_volatility: Decimal,
574    /// Underlying security symbol of the option
575    pub underlying_symbol: String,
576}
577
578impl TryFrom<quote::OptionQuote> for OptionQuote {
579    type Error = Error;
580
581    fn try_from(quote: quote::OptionQuote) -> Result<Self> {
582        let option_extend = quote.option_extend.unwrap_or_default();
583
584        Ok(Self {
585            symbol: quote.symbol,
586            last_done: quote.last_done.parse().unwrap_or_default(),
587            prev_close: quote.prev_close.parse().unwrap_or_default(),
588            open: quote.open.parse().unwrap_or_default(),
589            high: quote.high.parse().unwrap_or_default(),
590            low: quote.low.parse().unwrap_or_default(),
591            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
592                .map_err(|err| Error::parse_field_error("timestamp", err))?,
593            volume: quote.volume,
594            turnover: quote.turnover.parse().unwrap_or_default(),
595            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
596            implied_volatility: option_extend.implied_volatility.parse().unwrap_or_default(),
597            open_interest: option_extend.open_interest,
598            expiry_date: parse_date(&option_extend.expiry_date)
599                .map_err(|err| Error::parse_field_error("expiry_date", err))?,
600            strike_price: option_extend.strike_price.parse().unwrap_or_default(),
601            contract_multiplier: option_extend
602                .contract_multiplier
603                .parse()
604                .unwrap_or_default(),
605            contract_type: option_extend.contract_type.parse().unwrap_or_default(),
606            contract_size: option_extend.contract_size.parse().unwrap_or_default(),
607            direction: option_extend.direction.parse().unwrap_or_default(),
608            historical_volatility: option_extend
609                .historical_volatility
610                .parse()
611                .unwrap_or_default(),
612            underlying_symbol: option_extend.underlying_symbol,
613        })
614    }
615}
616
617/// Warrant type
618#[derive(
619    Debug,
620    Copy,
621    Clone,
622    Hash,
623    Eq,
624    PartialEq,
625    EnumString,
626    IntoPrimitive,
627    TryFromPrimitive,
628    Serialize,
629    Deserialize,
630)]
631#[repr(i32)]
632pub enum WarrantType {
633    /// Unknown
634    Unknown = -1,
635    /// Call
636    Call = 0,
637    /// Put
638    Put = 1,
639    /// Bull
640    Bull = 2,
641    /// Bear
642    Bear = 3,
643    /// Inline
644    Inline = 4,
645}
646
647/// Quote of warrant
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct WarrantQuote {
650    /// Security code
651    pub symbol: String,
652    /// Latest price
653    pub last_done: Decimal,
654    /// Yesterday's close
655    pub prev_close: Decimal,
656    /// Open
657    pub open: Decimal,
658    /// High
659    pub high: Decimal,
660    /// Low
661    pub low: Decimal,
662    /// Time of latest price
663    #[serde(with = "time::serde::rfc3339")]
664    pub timestamp: OffsetDateTime,
665    /// Volume
666    pub volume: i64,
667    /// Turnover
668    pub turnover: Decimal,
669    /// Security trading status
670    pub trade_status: TradeStatus,
671    /// Implied volatility
672    pub implied_volatility: Decimal,
673    /// Exprity date
674    pub expiry_date: Date,
675    /// Last tradalbe date
676    pub last_trade_date: Date,
677    /// Outstanding ratio
678    pub outstanding_ratio: Decimal,
679    /// Outstanding quantity
680    pub outstanding_quantity: i64,
681    /// Conversion ratio
682    pub conversion_ratio: Decimal,
683    /// Warrant type
684    pub category: WarrantType,
685    /// Strike price
686    pub strike_price: Decimal,
687    /// Upper bound price
688    pub upper_strike_price: Decimal,
689    /// Lower bound price
690    pub lower_strike_price: Decimal,
691    /// Call price
692    pub call_price: Decimal,
693    /// Underlying security symbol of the warrant
694    pub underlying_symbol: String,
695}
696
697impl TryFrom<quote::WarrantQuote> for WarrantQuote {
698    type Error = Error;
699
700    fn try_from(quote: quote::WarrantQuote) -> Result<Self> {
701        let warrant_extend = quote.warrant_extend.unwrap_or_default();
702
703        Ok(Self {
704            symbol: quote.symbol,
705            last_done: quote.last_done.parse().unwrap_or_default(),
706            prev_close: quote.prev_close.parse().unwrap_or_default(),
707            open: quote.open.parse().unwrap_or_default(),
708            high: quote.high.parse().unwrap_or_default(),
709            low: quote.low.parse().unwrap_or_default(),
710            timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
711                .map_err(|err| Error::parse_field_error("timestamp", err))?,
712            volume: quote.volume,
713            turnover: quote.turnover.parse().unwrap_or_default(),
714            trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
715            implied_volatility: warrant_extend
716                .implied_volatility
717                .parse()
718                .unwrap_or_default(),
719            expiry_date: parse_date(&warrant_extend.expiry_date)
720                .map_err(|err| Error::parse_field_error("expiry_date", err))?,
721            last_trade_date: parse_date(&warrant_extend.last_trade_date)
722                .map_err(|err| Error::parse_field_error("last_trade_date", err))?,
723            outstanding_ratio: warrant_extend.outstanding_ratio.parse().unwrap_or_default(),
724            outstanding_quantity: warrant_extend.outstanding_qty,
725            conversion_ratio: warrant_extend.conversion_ratio.parse().unwrap_or_default(),
726            category: warrant_extend.category.parse().unwrap_or_default(),
727            strike_price: warrant_extend.strike_price.parse().unwrap_or_default(),
728            upper_strike_price: warrant_extend
729                .upper_strike_price
730                .parse()
731                .unwrap_or_default(),
732            lower_strike_price: warrant_extend
733                .lower_strike_price
734                .parse()
735                .unwrap_or_default(),
736            call_price: warrant_extend.call_price.parse().unwrap_or_default(),
737            underlying_symbol: warrant_extend.underlying_symbol,
738        })
739    }
740}
741
742/// Security depth
743#[derive(Debug, Clone, Default, Serialize, Deserialize)]
744pub struct SecurityDepth {
745    /// Ask depth
746    pub asks: Vec<Depth>,
747    /// Bid depth
748    pub bids: Vec<Depth>,
749}
750
751/// Security brokers
752#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753pub struct SecurityBrokers {
754    /// Ask brokers
755    pub ask_brokers: Vec<Brokers>,
756    /// Bid brokers
757    pub bid_brokers: Vec<Brokers>,
758}
759
760/// Participant info
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct ParticipantInfo {
763    /// Broker IDs
764    pub broker_ids: Vec<i32>,
765    /// Participant name (zh-CN)
766    pub name_cn: String,
767    /// Participant name (en)
768    pub name_en: String,
769    /// Participant name (zh-HK)
770    pub name_hk: String,
771}
772
773impl From<quote::ParticipantInfo> for ParticipantInfo {
774    fn from(info: quote::ParticipantInfo) -> Self {
775        Self {
776            broker_ids: info.broker_ids,
777            name_cn: info.participant_name_cn,
778            name_en: info.participant_name_en,
779            name_hk: info.participant_name_hk,
780        }
781    }
782}
783
784/// Intraday line
785#[derive(Debug, Clone, Serialize, Deserialize)]
786pub struct IntradayLine {
787    /// Close price of the minute
788    pub price: Decimal,
789    /// Start time of the minute
790    #[serde(with = "time::serde::rfc3339")]
791    pub timestamp: OffsetDateTime,
792    /// Volume
793    pub volume: i64,
794    /// Turnover
795    pub turnover: Decimal,
796    /// Average price
797    pub avg_price: Decimal,
798}
799
800impl TryFrom<quote::Line> for IntradayLine {
801    type Error = Error;
802
803    fn try_from(value: quote::Line) -> Result<Self> {
804        Ok(Self {
805            price: value.price.parse().unwrap_or_default(),
806            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
807                .map_err(|err| Error::parse_field_error("timestamp", err))?,
808            volume: value.volume,
809            turnover: value.turnover.parse().unwrap_or_default(),
810            avg_price: value.avg_price.parse().unwrap_or_default(),
811        })
812    }
813}
814
815/// Candlestick
816#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
817pub struct Candlestick {
818    /// Close price
819    pub close: Decimal,
820    /// Open price
821    pub open: Decimal,
822    /// Low price
823    pub low: Decimal,
824    /// High price
825    pub high: Decimal,
826    /// Volume
827    pub volume: i64,
828    /// Turnover
829    pub turnover: Decimal,
830    /// Timestamp
831    #[serde(with = "time::serde::rfc3339")]
832    pub timestamp: OffsetDateTime,
833    /// Trade session
834    pub trade_session: TradeSession,
835    open_updated: bool,
836}
837
838impl TryFrom<quote::Candlestick> for Candlestick {
839    type Error = Error;
840
841    fn try_from(value: quote::Candlestick) -> Result<Self> {
842        Ok(Self {
843            close: value.close.parse().unwrap_or_default(),
844            open: value.open.parse().unwrap_or_default(),
845            low: value.low.parse().unwrap_or_default(),
846            high: value.high.parse().unwrap_or_default(),
847            volume: value.volume,
848            turnover: value.turnover.parse().unwrap_or_default(),
849            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
850                .map_err(|err| Error::parse_field_error("timestamp", err))?,
851            trade_session: longbridge_proto::quote::TradeSession::try_from(value.trade_session)
852                .map_err(|err| Error::parse_field_error("trade_session", err))?
853                .into(),
854            open_updated: true,
855        })
856    }
857}
858
859impl longbridge_candlesticks::CandlestickType for Candlestick {
860    type PriceType = Decimal;
861    type VolumeType = i64;
862    type TurnoverType = Decimal;
863    type TradeSessionType = TradeSession;
864
865    #[inline]
866    fn new(
867        components: CandlestickComponents<
868            Self::PriceType,
869            Self::VolumeType,
870            Self::TurnoverType,
871            Self::TradeSessionType,
872        >,
873    ) -> Self {
874        Self {
875            timestamp: components.time,
876            open: components.open,
877            high: components.high,
878            low: components.low,
879            close: components.close,
880            volume: components.volume,
881            turnover: components.turnover,
882            trade_session: components.trade_session,
883            open_updated: components.open_updated,
884        }
885    }
886
887    #[inline]
888    fn time(&self) -> OffsetDateTime {
889        self.timestamp
890    }
891
892    #[inline]
893    fn set_time(&mut self, time: OffsetDateTime) {
894        self.timestamp = time;
895    }
896
897    #[inline]
898    fn open(&self) -> Self::PriceType {
899        self.open
900    }
901
902    #[inline]
903    fn set_open(&mut self, open: Self::PriceType) {
904        self.open = open;
905    }
906
907    #[inline]
908    fn high(&self) -> Self::PriceType {
909        self.high
910    }
911
912    #[inline]
913    fn set_high(&mut self, high: Self::PriceType) {
914        self.high = high;
915    }
916
917    #[inline]
918    fn low(&self) -> Self::PriceType {
919        self.low
920    }
921
922    #[inline]
923    fn set_low(&mut self, low: Self::PriceType) {
924        self.low = low;
925    }
926
927    #[inline]
928    fn close(&self) -> Self::PriceType {
929        self.close
930    }
931
932    #[inline]
933    fn set_close(&mut self, close: Self::PriceType) {
934        self.close = close;
935    }
936
937    #[inline]
938    fn volume(&self) -> Self::VolumeType {
939        self.volume
940    }
941
942    #[inline]
943    fn set_volume(&mut self, volume: Self::VolumeType) {
944        self.volume = volume;
945    }
946
947    #[inline]
948    fn turnover(&self) -> Self::TurnoverType {
949        self.turnover
950    }
951
952    #[inline]
953    fn set_turnover(&mut self, turnover: Self::TurnoverType) {
954        self.turnover = turnover;
955    }
956
957    #[inline]
958    fn trade_session(&self) -> Self::TradeSessionType {
959        self.trade_session
960    }
961
962    #[inline]
963    fn set_open_updated(&mut self, updated: bool) {
964        self.open_updated = updated;
965    }
966
967    #[inline]
968    fn open_updated(&self) -> bool {
969        self.open_updated
970    }
971}
972
973/// A single option contract of an option chain
974///
975/// Every contract is an independent entry: calls and puts are not paired, so a
976/// strike price that is listed on one side only yields a single entry.
977#[derive(Debug, Clone, Serialize, Deserialize)]
978pub struct OptionChainContract {
979    /// Option contract code, in `ticker.region` format
980    pub symbol: String,
981    /// Expiry date, in US Eastern time
982    pub expiry_date: Date,
983    /// Strike price
984    pub strike_price: Decimal,
985    /// Contract direction
986    pub direction: OptionDirection,
987    /// Special expiration cycle of the contract
988    pub option_type: OptionExpiryCycleType,
989    /// Whether the contract is a legacy contract left over from a corporate
990    /// action
991    pub standard_attr: OptionStandardAttr,
992    /// Number of days remaining until the option expires, updated daily at
993    /// midnight ET
994    ///
995    /// `0` for options expiring today, and negative for already-expired
996    /// options.
997    pub days_to_expiry: i32,
998}
999
1000/// Issuer info
1001#[derive(Debug, Clone, Serialize, Deserialize)]
1002pub struct IssuerInfo {
1003    /// Issuer ID
1004    pub issuer_id: i32,
1005    /// Issuer name (zh-CN)
1006    pub name_cn: String,
1007    /// Issuer name (en)
1008    pub name_en: String,
1009    /// Issuer name (zh-HK)
1010    pub name_hk: String,
1011}
1012
1013impl From<quote::IssuerInfo> for IssuerInfo {
1014    fn from(info: quote::IssuerInfo) -> Self {
1015        Self {
1016            issuer_id: info.id,
1017            name_cn: info.name_cn,
1018            name_en: info.name_en,
1019            name_hk: info.name_hk,
1020        }
1021    }
1022}
1023
1024/// Sort order type
1025#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1026#[repr(i32)]
1027pub enum SortOrderType {
1028    /// Ascending
1029    Ascending = 0,
1030    /// Descending
1031    Descending = 1,
1032}
1033
1034/// Warrant sort by
1035#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1036#[repr(i32)]
1037pub enum WarrantSortBy {
1038    /// Last done
1039    LastDone = 0,
1040    /// Change rate
1041    ChangeRate = 1,
1042    /// Change value
1043    ChangeValue = 2,
1044    /// Volume
1045    Volume = 3,
1046    /// Turnover
1047    Turnover = 4,
1048    /// Expiry date
1049    ExpiryDate = 5,
1050    /// Strike price
1051    StrikePrice = 6,
1052    /// Upper strike price
1053    UpperStrikePrice = 7,
1054    /// Lower strike price
1055    LowerStrikePrice = 8,
1056    /// Outstanding quantity
1057    OutstandingQuantity = 9,
1058    /// Outstanding ratio
1059    OutstandingRatio = 10,
1060    /// Premium
1061    Premium = 11,
1062    /// In/out of the bound
1063    ItmOtm = 12,
1064    /// Implied volatility
1065    ImpliedVolatility = 13,
1066    /// Greek value Delta
1067    Delta = 14,
1068    /// Call price
1069    CallPrice = 15,
1070    /// Price interval from the call price
1071    ToCallPrice = 16,
1072    /// Effective leverage
1073    EffectiveLeverage = 17,
1074    /// Leverage ratio
1075    LeverageRatio = 18,
1076    /// Conversion ratio
1077    ConversionRatio = 19,
1078    /// Breakeven point
1079    BalancePoint = 20,
1080    /// Status
1081    Status = 21,
1082}
1083
1084/// Filter warrant expiry date type
1085#[allow(non_camel_case_types)]
1086#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1087#[repr(i32)]
1088pub enum FilterWarrantExpiryDate {
1089    /// Less than 3 months
1090    LT_3 = 1,
1091    /// 3 - 6 months
1092    Between_3_6 = 2,
1093    /// 6 - 12 months
1094    Between_6_12 = 3,
1095    /// Greater than 12 months
1096    GT_12 = 4,
1097}
1098
1099/// Filter warrant in/out of the bounds type
1100#[allow(non_camel_case_types)]
1101#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
1102#[repr(i32)]
1103pub enum FilterWarrantInOutBoundsType {
1104    /// In bounds
1105    In = 1,
1106    /// Out bounds
1107    Out = 2,
1108}
1109
1110/// Warrant status
1111#[derive(
1112    Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
1113)]
1114#[repr(i32)]
1115pub enum WarrantStatus {
1116    /// Unknown
1117    ///
1118    /// The server reports an unrecognized status (e.g. `0` on placeholder
1119    /// rows).
1120    Unknown = 0,
1121    /// Suspend
1122    Suspend = 2,
1123    /// Prepare List
1124    PrepareList = 3,
1125    /// Normal
1126    Normal = 4,
1127}
1128
1129/// Warrant info
1130#[derive(Debug, Clone, Serialize, Deserialize)]
1131pub struct WarrantInfo {
1132    /// Security code
1133    pub symbol: String,
1134    /// Warrant type
1135    pub warrant_type: WarrantType,
1136    /// Security name
1137    pub name: String,
1138    /// Latest price
1139    pub last_done: Decimal,
1140    /// Quote change rate
1141    pub change_rate: Decimal,
1142    /// Quote change
1143    pub change_value: Decimal,
1144    /// Volume
1145    pub volume: i64,
1146    /// Turnover
1147    pub turnover: Decimal,
1148    /// Expiry date
1149    ///
1150    /// `None` if the server does not report an expiry date for this warrant.
1151    pub expiry_date: Option<Date>,
1152    /// Strike price
1153    pub strike_price: Option<Decimal>,
1154    /// Upper strike price
1155    pub upper_strike_price: Option<Decimal>,
1156    /// Lower strike price
1157    pub lower_strike_price: Option<Decimal>,
1158    /// Outstanding quantity
1159    pub outstanding_qty: i64,
1160    /// Outstanding ratio
1161    pub outstanding_ratio: Decimal,
1162    /// Premium
1163    pub premium: Decimal,
1164    /// In/out of the bound
1165    pub itm_otm: Option<Decimal>,
1166    /// Implied volatility
1167    pub implied_volatility: Option<Decimal>,
1168    /// Delta
1169    pub delta: Option<Decimal>,
1170    /// Call price
1171    pub call_price: Option<Decimal>,
1172    /// Price interval from the call price
1173    pub to_call_price: Option<Decimal>,
1174    /// Effective leverage
1175    pub effective_leverage: Option<Decimal>,
1176    /// Leverage ratio
1177    pub leverage_ratio: Decimal,
1178    /// Conversion ratio
1179    pub conversion_ratio: Option<Decimal>,
1180    /// Breakeven point
1181    pub balance_point: Option<Decimal>,
1182    /// Status
1183    pub status: WarrantStatus,
1184}
1185
1186impl TryFrom<quote::FilterWarrant> for WarrantInfo {
1187    type Error = Error;
1188
1189    fn try_from(info: quote::FilterWarrant) -> Result<Self> {
1190        let r#type = WarrantType::try_from(info.r#type)
1191            .map_err(|err| Error::parse_field_error("type", err))?;
1192
1193        match r#type {
1194            WarrantType::Unknown => unreachable!(),
1195            WarrantType::Call | WarrantType::Put => Ok(Self {
1196                symbol: info.symbol,
1197                warrant_type: r#type,
1198                name: info.name,
1199                last_done: info.last_done.parse().unwrap_or_default(),
1200                change_rate: info.change_rate.parse().unwrap_or_default(),
1201                change_value: info.change_val.parse().unwrap_or_default(),
1202                volume: info.volume,
1203                turnover: info.turnover.parse().unwrap_or_default(),
1204                expiry_date: parse_date(&info.expiry_date).ok(),
1205                strike_price: info.strike_price.parse().ok(),
1206                upper_strike_price: info.upper_strike_price.parse().ok(),
1207                lower_strike_price: info.lower_strike_price.parse().ok(),
1208                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1209                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1210                premium: info.premium.parse().unwrap_or_default(),
1211                itm_otm: info.itm_otm.parse().ok(),
1212                implied_volatility: info.implied_volatility.parse().ok(),
1213                delta: info.delta.parse().ok(),
1214                call_price: info.call_price.parse().ok(),
1215                to_call_price: info.to_call_price.parse().ok(),
1216                effective_leverage: info.effective_leverage.parse().ok(),
1217                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1218                conversion_ratio: info.conversion_ratio.parse().ok(),
1219                balance_point: info.balance_point.parse().ok(),
1220                status: WarrantStatus::try_from(info.status).unwrap_or(WarrantStatus::Unknown),
1221            }),
1222            WarrantType::Bull | WarrantType::Bear => Ok(Self {
1223                symbol: info.symbol,
1224                warrant_type: r#type,
1225                name: info.name,
1226                last_done: info.last_done.parse().unwrap_or_default(),
1227                change_rate: info.change_rate.parse().unwrap_or_default(),
1228                change_value: info.change_val.parse().unwrap_or_default(),
1229                volume: info.volume,
1230                turnover: info.turnover.parse().unwrap_or_default(),
1231                expiry_date: parse_date(&info.expiry_date).ok(),
1232                strike_price: Some(info.strike_price.parse().unwrap_or_default()),
1233                upper_strike_price: None,
1234                lower_strike_price: None,
1235                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1236                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1237                premium: info.premium.parse().unwrap_or_default(),
1238                itm_otm: Some(info.itm_otm.parse().unwrap_or_default()),
1239                implied_volatility: None,
1240                delta: None,
1241                call_price: Some(info.call_price.parse().unwrap_or_default()),
1242                to_call_price: Some(info.to_call_price.parse().unwrap_or_default()),
1243                effective_leverage: None,
1244                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1245                conversion_ratio: Some(info.conversion_ratio.parse().unwrap_or_default()),
1246                balance_point: Some(info.balance_point.parse().unwrap_or_default()),
1247                status: WarrantStatus::try_from(info.status).unwrap_or(WarrantStatus::Unknown),
1248            }),
1249            WarrantType::Inline => Ok(Self {
1250                symbol: info.symbol,
1251                warrant_type: r#type,
1252                name: info.name,
1253                last_done: info.last_done.parse().unwrap_or_default(),
1254                change_rate: info.change_rate.parse().unwrap_or_default(),
1255                change_value: info.change_val.parse().unwrap_or_default(),
1256                volume: info.volume,
1257                turnover: info.turnover.parse().unwrap_or_default(),
1258                expiry_date: parse_date(&info.expiry_date).ok(),
1259                strike_price: None,
1260                upper_strike_price: Some(info.upper_strike_price.parse().unwrap_or_default()),
1261                lower_strike_price: Some(info.lower_strike_price.parse().unwrap_or_default()),
1262                outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
1263                outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
1264                premium: info.premium.parse().unwrap_or_default(),
1265                itm_otm: None,
1266                implied_volatility: None,
1267                delta: None,
1268                call_price: None,
1269                to_call_price: None,
1270                effective_leverage: None,
1271                leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
1272                conversion_ratio: None,
1273                balance_point: None,
1274                status: WarrantStatus::try_from(info.status).unwrap_or(WarrantStatus::Unknown),
1275            }),
1276        }
1277    }
1278}
1279
1280/// The information of trading session
1281#[derive(Debug, Clone, Serialize, Deserialize)]
1282pub struct TradingSessionInfo {
1283    /// Being trading time
1284    pub begin_time: Time,
1285    /// End trading time
1286    pub end_time: Time,
1287    /// Trading session
1288    pub trade_session: TradeSession,
1289}
1290
1291impl TryFrom<quote::TradePeriod> for TradingSessionInfo {
1292    type Error = Error;
1293
1294    fn try_from(value: quote::TradePeriod) -> Result<Self> {
1295        #[inline]
1296        fn parse_time(value: i32) -> ::std::result::Result<Time, time::error::ComponentRange> {
1297            Time::from_hms(((value / 100) % 100) as u8, (value % 100) as u8, 0)
1298        }
1299
1300        Ok(Self {
1301            begin_time: parse_time(value.beg_time)
1302                .map_err(|err| Error::parse_field_error("beg_time", err))?,
1303            end_time: parse_time(value.end_time)
1304                .map_err(|err| Error::parse_field_error("end_time", err))?,
1305            trade_session: longbridge_proto::quote::TradeSession::try_from(value.trade_session)
1306                .unwrap_or_default()
1307                .into(),
1308        })
1309    }
1310}
1311
1312/// Market trading session
1313#[derive(Debug, Clone, Serialize, Deserialize)]
1314pub struct MarketTradingSession {
1315    /// Market
1316    pub market: Market,
1317    /// Trading session
1318    pub trade_sessions: Vec<TradingSessionInfo>,
1319}
1320
1321impl TryFrom<quote::MarketTradePeriod> for MarketTradingSession {
1322    type Error = Error;
1323
1324    fn try_from(value: quote::MarketTradePeriod) -> Result<Self> {
1325        Ok(Self {
1326            market: value.market.parse().unwrap_or_default(),
1327            trade_sessions: value
1328                .trade_session
1329                .into_iter()
1330                .map(TryInto::try_into)
1331                .collect::<Result<Vec<_>>>()?,
1332        })
1333    }
1334}
1335
1336/// Market trading days
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1338pub struct MarketTradingDays {
1339    /// Trading days
1340    pub trading_days: Vec<Date>,
1341    /// Half trading days
1342    pub half_trading_days: Vec<Date>,
1343}
1344
1345/// Capital flow line
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1347pub struct CapitalFlowLine {
1348    /// Inflow capital data
1349    pub inflow: Decimal,
1350    /// Time
1351    pub timestamp: OffsetDateTime,
1352}
1353
1354impl TryFrom<quote::capital_flow_intraday_response::CapitalFlowLine> for CapitalFlowLine {
1355    type Error = Error;
1356
1357    fn try_from(value: quote::capital_flow_intraday_response::CapitalFlowLine) -> Result<Self> {
1358        Ok(Self {
1359            inflow: value.inflow.parse().unwrap_or_default(),
1360            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
1361                .map_err(|err| Error::parse_field_error("timestamp", err))?,
1362        })
1363    }
1364}
1365
1366/// Capital distribution
1367#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1368pub struct CapitalDistribution {
1369    /// Large order
1370    pub large: Decimal,
1371    /// Medium order
1372    pub medium: Decimal,
1373    /// Small order
1374    pub small: Decimal,
1375}
1376
1377impl TryFrom<quote::capital_distribution_response::CapitalDistribution> for CapitalDistribution {
1378    type Error = Error;
1379
1380    fn try_from(value: quote::capital_distribution_response::CapitalDistribution) -> Result<Self> {
1381        Ok(Self {
1382            large: value.large.parse().unwrap_or_default(),
1383            medium: value.medium.parse().unwrap_or_default(),
1384            small: value.small.parse().unwrap_or_default(),
1385        })
1386    }
1387}
1388
1389/// Capital distribution response
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct CapitalDistributionResponse {
1392    /// Time
1393    pub timestamp: OffsetDateTime,
1394    /// Inflow capital data
1395    pub capital_in: CapitalDistribution,
1396    /// Outflow capital data
1397    pub capital_out: CapitalDistribution,
1398}
1399
1400impl TryFrom<quote::CapitalDistributionResponse> for CapitalDistributionResponse {
1401    type Error = Error;
1402
1403    fn try_from(value: quote::CapitalDistributionResponse) -> Result<Self> {
1404        Ok(Self {
1405            timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
1406                .map_err(|err| Error::parse_field_error("timestamp", err))?,
1407            capital_in: value
1408                .capital_in
1409                .map(TryInto::try_into)
1410                .transpose()?
1411                .unwrap_or_default(),
1412            capital_out: value
1413                .capital_out
1414                .map(TryInto::try_into)
1415                .transpose()?
1416                .unwrap_or_default(),
1417        })
1418    }
1419}
1420
1421/// Watchlist security
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1423pub struct WatchlistSecurity {
1424    /// Security symbol
1425    pub symbol: String,
1426    /// Market
1427    pub market: Market,
1428    /// Security name
1429    pub name: String,
1430    /// Watched price
1431    #[serde(with = "serde_utils::decimal_opt_empty_is_none")]
1432    pub watched_price: Option<Decimal>,
1433    /// Watched time
1434    #[serde(
1435        serialize_with = "time::serde::rfc3339::serialize",
1436        deserialize_with = "serde_utils::timestamp::deserialize"
1437    )]
1438    pub watched_at: OffsetDateTime,
1439    /// Whether the security is pinned to the top of the group
1440    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1441    pub is_pinned: bool,
1442}
1443
1444/// Watchlist group
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1446pub struct WatchlistGroup {
1447    /// Group id
1448    #[serde(with = "serde_utils::int64_str")]
1449    pub id: i64,
1450    /// Group name
1451    pub name: String,
1452    /// Securities
1453    pub securities: Vec<WatchlistSecurity>,
1454}
1455
1456/// An request for create watchlist group
1457#[derive(Debug, Clone)]
1458pub struct RequestCreateWatchlistGroup {
1459    /// Group name
1460    pub name: String,
1461    /// Securities
1462    pub securities: Option<Vec<String>>,
1463}
1464
1465impl RequestCreateWatchlistGroup {
1466    /// Create a new request for create watchlist group
1467    pub fn new(name: impl Into<String>) -> Self {
1468        Self {
1469            name: name.into(),
1470            securities: None,
1471        }
1472    }
1473
1474    /// Set securities to the request
1475    pub fn securities<I, T>(self, securities: I) -> Self
1476    where
1477        I: IntoIterator<Item = T>,
1478        T: Into<String>,
1479    {
1480        Self {
1481            securities: Some(securities.into_iter().map(Into::into).collect()),
1482            ..self
1483        }
1484    }
1485}
1486
1487/// Securities update mode
1488#[derive(Debug, Copy, Clone, Default, Serialize)]
1489#[serde(rename_all = "lowercase")]
1490pub enum SecuritiesUpdateMode {
1491    /// Add securities
1492    Add,
1493    /// Remove securities
1494    Remove,
1495    /// Replace securities
1496    #[default]
1497    Replace,
1498}
1499
1500/// An request for update watchlist group
1501#[derive(Debug, Clone)]
1502pub struct RequestUpdateWatchlistGroup {
1503    /// Group id
1504    pub id: i64,
1505    /// Group name
1506    pub name: Option<String>,
1507    /// Securities
1508    pub securities: Option<Vec<String>>,
1509    /// Securities Update mode
1510    pub mode: SecuritiesUpdateMode,
1511}
1512
1513impl RequestUpdateWatchlistGroup {
1514    /// Create a new request for update watchlist group
1515    #[inline]
1516    pub fn new(id: i64) -> Self {
1517        Self {
1518            id,
1519            name: None,
1520            securities: None,
1521            mode: SecuritiesUpdateMode::default(),
1522        }
1523    }
1524
1525    /// Set group name to the request
1526    pub fn name(self, name: impl Into<String>) -> Self {
1527        Self {
1528            name: Some(name.into()),
1529            ..self
1530        }
1531    }
1532
1533    /// Set securities to the request
1534    pub fn securities<I, T>(self, securities: I) -> Self
1535    where
1536        I: IntoIterator<Item = T>,
1537        T: Into<String>,
1538    {
1539        Self {
1540            securities: Some(securities.into_iter().map(Into::into).collect()),
1541            ..self
1542        }
1543    }
1544
1545    /// Set securities update mode to the request
1546    pub fn mode(self, mode: SecuritiesUpdateMode) -> Self {
1547        Self { mode, ..self }
1548    }
1549}
1550
1551/// Calc index
1552#[derive(Debug, Copy, Clone, Eq, PartialEq)]
1553pub enum CalcIndex {
1554    /// Latest price
1555    LastDone,
1556    /// Change value
1557    ChangeValue,
1558    /// Change rate
1559    ChangeRate,
1560    /// Volume
1561    Volume,
1562    /// Turnover
1563    Turnover,
1564    /// Year-to-date change ratio
1565    YtdChangeRate,
1566    /// Turnover rate
1567    TurnoverRate,
1568    /// Total market value
1569    TotalMarketValue,
1570    /// Capital flow
1571    CapitalFlow,
1572    /// Amplitude
1573    Amplitude,
1574    /// Volume ratio
1575    VolumeRatio,
1576    /// PE (TTM)
1577    PeTtmRatio,
1578    /// PB
1579    PbRatio,
1580    /// Dividend ratio (TTM)
1581    DividendRatioTtm,
1582    /// Five days change ratio
1583    FiveDayChangeRate,
1584    /// Ten days change ratio
1585    TenDayChangeRate,
1586    /// Half year change ratio
1587    HalfYearChangeRate,
1588    /// Five minutes change ratio
1589    FiveMinutesChangeRate,
1590    /// Expiry date
1591    ExpiryDate,
1592    /// Strike price
1593    StrikePrice,
1594    /// Upper bound price
1595    UpperStrikePrice,
1596    /// Lower bound price
1597    LowerStrikePrice,
1598    /// Outstanding quantity
1599    OutstandingQty,
1600    /// Outstanding ratio
1601    OutstandingRatio,
1602    /// Premium
1603    Premium,
1604    /// In/out of the bound
1605    ItmOtm,
1606    /// Implied volatility
1607    ImpliedVolatility,
1608    /// Warrant delta
1609    WarrantDelta,
1610    /// Call price
1611    CallPrice,
1612    /// Price interval from the call price
1613    ToCallPrice,
1614    /// Effective leverage
1615    EffectiveLeverage,
1616    /// Leverage ratio
1617    LeverageRatio,
1618    /// Conversion ratio
1619    ConversionRatio,
1620    /// Breakeven point
1621    BalancePoint,
1622    /// Open interest
1623    OpenInterest,
1624    /// Delta
1625    Delta,
1626    /// Gamma
1627    Gamma,
1628    /// Theta
1629    Theta,
1630    /// Vega
1631    Vega,
1632    /// Rho
1633    Rho,
1634}
1635
1636impl From<CalcIndex> for longbridge_proto::quote::CalcIndex {
1637    fn from(value: CalcIndex) -> Self {
1638        use longbridge_proto::quote::CalcIndex::*;
1639
1640        match value {
1641            CalcIndex::LastDone => CalcindexLastDone,
1642            CalcIndex::ChangeValue => CalcindexChangeVal,
1643            CalcIndex::ChangeRate => CalcindexChangeRate,
1644            CalcIndex::Volume => CalcindexVolume,
1645            CalcIndex::Turnover => CalcindexTurnover,
1646            CalcIndex::YtdChangeRate => CalcindexYtdChangeRate,
1647            CalcIndex::TurnoverRate => CalcindexTurnoverRate,
1648            CalcIndex::TotalMarketValue => CalcindexTotalMarketValue,
1649            CalcIndex::CapitalFlow => CalcindexCapitalFlow,
1650            CalcIndex::Amplitude => CalcindexAmplitude,
1651            CalcIndex::VolumeRatio => CalcindexVolumeRatio,
1652            CalcIndex::PeTtmRatio => CalcindexPeTtmRatio,
1653            CalcIndex::PbRatio => CalcindexPbRatio,
1654            CalcIndex::DividendRatioTtm => CalcindexDividendRatioTtm,
1655            CalcIndex::FiveDayChangeRate => CalcindexFiveDayChangeRate,
1656            CalcIndex::TenDayChangeRate => CalcindexTenDayChangeRate,
1657            CalcIndex::HalfYearChangeRate => CalcindexHalfYearChangeRate,
1658            CalcIndex::FiveMinutesChangeRate => CalcindexFiveMinutesChangeRate,
1659            CalcIndex::ExpiryDate => CalcindexExpiryDate,
1660            CalcIndex::StrikePrice => CalcindexStrikePrice,
1661            CalcIndex::UpperStrikePrice => CalcindexUpperStrikePrice,
1662            CalcIndex::LowerStrikePrice => CalcindexLowerStrikePrice,
1663            CalcIndex::OutstandingQty => CalcindexOutstandingQty,
1664            CalcIndex::OutstandingRatio => CalcindexOutstandingRatio,
1665            CalcIndex::Premium => CalcindexPremium,
1666            CalcIndex::ItmOtm => CalcindexItmOtm,
1667            CalcIndex::ImpliedVolatility => CalcindexImpliedVolatility,
1668            CalcIndex::WarrantDelta => CalcindexWarrantDelta,
1669            CalcIndex::CallPrice => CalcindexCallPrice,
1670            CalcIndex::ToCallPrice => CalcindexToCallPrice,
1671            CalcIndex::EffectiveLeverage => CalcindexEffectiveLeverage,
1672            CalcIndex::LeverageRatio => CalcindexLeverageRatio,
1673            CalcIndex::ConversionRatio => CalcindexConversionRatio,
1674            CalcIndex::BalancePoint => CalcindexBalancePoint,
1675            CalcIndex::OpenInterest => CalcindexOpenInterest,
1676            CalcIndex::Delta => CalcindexDelta,
1677            CalcIndex::Gamma => CalcindexGamma,
1678            CalcIndex::Theta => CalcindexTheta,
1679            CalcIndex::Vega => CalcindexVega,
1680            CalcIndex::Rho => CalcindexRho,
1681        }
1682    }
1683}
1684
1685/// Security calc index response
1686#[derive(Debug, Clone, Serialize, Deserialize)]
1687pub struct SecurityCalcIndex {
1688    /// Security code
1689    pub symbol: String,
1690    /// Latest price
1691    pub last_done: Option<Decimal>,
1692    /// Change value
1693    pub change_value: Option<Decimal>,
1694    /// Change ratio
1695    pub change_rate: Option<Decimal>,
1696    /// Volume
1697    pub volume: Option<i64>,
1698    /// Turnover
1699    pub turnover: Option<Decimal>,
1700    /// Year-to-date change ratio
1701    pub ytd_change_rate: Option<Decimal>,
1702    /// Turnover rate
1703    pub turnover_rate: Option<Decimal>,
1704    /// Total market value
1705    pub total_market_value: Option<Decimal>,
1706    /// Capital flow
1707    pub capital_flow: Option<Decimal>,
1708    /// Amplitude
1709    pub amplitude: Option<Decimal>,
1710    /// Volume ratio
1711    pub volume_ratio: Option<Decimal>,
1712    /// PE (TTM)
1713    pub pe_ttm_ratio: Option<Decimal>,
1714    /// PB
1715    pub pb_ratio: Option<Decimal>,
1716    /// Dividend ratio (TTM)
1717    pub dividend_ratio_ttm: Option<Decimal>,
1718    /// Five days change ratio
1719    pub five_day_change_rate: Option<Decimal>,
1720    /// Ten days change ratio
1721    pub ten_day_change_rate: Option<Decimal>,
1722    /// Half year change ratio
1723    pub half_year_change_rate: Option<Decimal>,
1724    /// Five minutes change ratio
1725    pub five_minutes_change_rate: Option<Decimal>,
1726    /// Expiry date
1727    pub expiry_date: Option<Date>,
1728    /// Strike price
1729    pub strike_price: Option<Decimal>,
1730    /// Upper bound price
1731    pub upper_strike_price: Option<Decimal>,
1732    /// Lower bound price
1733    pub lower_strike_price: Option<Decimal>,
1734    /// Outstanding quantity
1735    pub outstanding_qty: Option<i64>,
1736    /// Outstanding ratio
1737    pub outstanding_ratio: Option<Decimal>,
1738    /// Premium
1739    pub premium: Option<Decimal>,
1740    /// In/out of the bound
1741    pub itm_otm: Option<Decimal>,
1742    /// Implied volatility
1743    pub implied_volatility: Option<Decimal>,
1744    /// Warrant delta
1745    pub warrant_delta: Option<Decimal>,
1746    /// Call price
1747    pub call_price: Option<Decimal>,
1748    /// Price interval from the call price
1749    pub to_call_price: Option<Decimal>,
1750    /// Effective leverage
1751    pub effective_leverage: Option<Decimal>,
1752    /// Leverage ratio
1753    pub leverage_ratio: Option<Decimal>,
1754    /// Conversion ratio
1755    pub conversion_ratio: Option<Decimal>,
1756    /// Breakeven point
1757    pub balance_point: Option<Decimal>,
1758    /// Open interest
1759    pub open_interest: Option<i64>,
1760    /// Delta. Measures the expected change in option price for a $1 move in the
1761    /// underlying asset price.
1762    pub delta: Option<Decimal>,
1763    /// Gamma. Measures the expected change in Delta for a $1 move in the
1764    /// underlying asset price.
1765    pub gamma: Option<Decimal>,
1766    /// Theta. Measures the expected change in option price as one day passes;
1767    /// the raw value has been divided by 365 to convert to a daily value,
1768    /// representing the impact of one day's time decay on the option price.
1769    pub theta: Option<Decimal>,
1770    /// Vega. Measures the expected change in option price when implied
1771    /// volatility (IV) moves by 1 (i.e. 100%); divide the raw value by 100 to
1772    /// get the expected price change per 1% move in IV.
1773    pub vega: Option<Decimal>,
1774    /// Rho. Measures the expected change in option price when the risk-free
1775    /// interest rate moves by 1 (i.e. 100%); divide the raw value by 100 to get
1776    /// the expected price change per 1% move in the interest rate.
1777    pub rho: Option<Decimal>,
1778}
1779
1780impl SecurityCalcIndex {
1781    pub(crate) fn from_proto(
1782        resp: longbridge_proto::quote::SecurityCalcIndex,
1783        indexes: &[CalcIndex],
1784    ) -> Self {
1785        let mut output = SecurityCalcIndex {
1786            symbol: resp.symbol,
1787            last_done: None,
1788            change_value: None,
1789            change_rate: None,
1790            volume: None,
1791            turnover: None,
1792            ytd_change_rate: None,
1793            turnover_rate: None,
1794            total_market_value: None,
1795            capital_flow: None,
1796            amplitude: None,
1797            volume_ratio: None,
1798            pe_ttm_ratio: None,
1799            pb_ratio: None,
1800            dividend_ratio_ttm: None,
1801            five_day_change_rate: None,
1802            ten_day_change_rate: None,
1803            half_year_change_rate: None,
1804            five_minutes_change_rate: None,
1805            expiry_date: None,
1806            strike_price: None,
1807            upper_strike_price: None,
1808            lower_strike_price: None,
1809            outstanding_qty: None,
1810            outstanding_ratio: None,
1811            premium: None,
1812            itm_otm: None,
1813            implied_volatility: None,
1814            warrant_delta: None,
1815            call_price: None,
1816            to_call_price: None,
1817            effective_leverage: None,
1818            leverage_ratio: None,
1819            conversion_ratio: None,
1820            balance_point: None,
1821            open_interest: None,
1822            delta: None,
1823            gamma: None,
1824            theta: None,
1825            vega: None,
1826            rho: None,
1827        };
1828
1829        for index in indexes {
1830            match index {
1831                CalcIndex::LastDone => output.last_done = resp.last_done.parse().ok(),
1832                CalcIndex::ChangeValue => output.change_value = resp.change_val.parse().ok(),
1833                CalcIndex::ChangeRate => output.change_rate = resp.change_rate.parse().ok(),
1834                CalcIndex::Volume => output.volume = Some(resp.volume),
1835                CalcIndex::Turnover => output.turnover = resp.turnover.parse().ok(),
1836                CalcIndex::YtdChangeRate => {
1837                    output.ytd_change_rate = resp.ytd_change_rate.parse().ok()
1838                }
1839                CalcIndex::TurnoverRate => output.turnover_rate = resp.turnover_rate.parse().ok(),
1840                CalcIndex::TotalMarketValue => {
1841                    output.total_market_value = resp.total_market_value.parse().ok()
1842                }
1843                CalcIndex::CapitalFlow => output.capital_flow = resp.capital_flow.parse().ok(),
1844                CalcIndex::Amplitude => output.amplitude = resp.amplitude.parse().ok(),
1845                CalcIndex::VolumeRatio => output.volume_ratio = resp.volume_ratio.parse().ok(),
1846                CalcIndex::PeTtmRatio => output.pe_ttm_ratio = resp.pe_ttm_ratio.parse().ok(),
1847                CalcIndex::PbRatio => output.pb_ratio = resp.pb_ratio.parse().ok(),
1848                CalcIndex::DividendRatioTtm => {
1849                    output.dividend_ratio_ttm = resp.dividend_ratio_ttm.parse().ok()
1850                }
1851                CalcIndex::FiveDayChangeRate => {
1852                    output.five_day_change_rate = resp.five_day_change_rate.parse().ok()
1853                }
1854                CalcIndex::TenDayChangeRate => {
1855                    output.ten_day_change_rate = resp.ten_day_change_rate.parse().ok()
1856                }
1857                CalcIndex::HalfYearChangeRate => {
1858                    output.half_year_change_rate = resp.half_year_change_rate.parse().ok()
1859                }
1860                CalcIndex::FiveMinutesChangeRate => {
1861                    output.five_minutes_change_rate = resp.five_minutes_change_rate.parse().ok()
1862                }
1863                CalcIndex::ExpiryDate => output.expiry_date = parse_date(&resp.expiry_date).ok(),
1864                CalcIndex::StrikePrice => output.strike_price = resp.strike_price.parse().ok(),
1865                CalcIndex::UpperStrikePrice => {
1866                    output.upper_strike_price = resp.upper_strike_price.parse().ok()
1867                }
1868                CalcIndex::LowerStrikePrice => {
1869                    output.lower_strike_price = resp.lower_strike_price.parse().ok()
1870                }
1871                CalcIndex::OutstandingQty => output.outstanding_qty = Some(resp.outstanding_qty),
1872                CalcIndex::OutstandingRatio => {
1873                    output.outstanding_ratio = resp.outstanding_ratio.parse().ok()
1874                }
1875                CalcIndex::Premium => output.premium = resp.premium.parse().ok(),
1876                CalcIndex::ItmOtm => output.itm_otm = resp.itm_otm.parse().ok(),
1877                CalcIndex::ImpliedVolatility => {
1878                    output.implied_volatility = resp.implied_volatility.parse().ok()
1879                }
1880                CalcIndex::WarrantDelta => output.warrant_delta = resp.warrant_delta.parse().ok(),
1881                CalcIndex::CallPrice => output.call_price = resp.call_price.parse().ok(),
1882                CalcIndex::ToCallPrice => output.to_call_price = resp.to_call_price.parse().ok(),
1883                CalcIndex::EffectiveLeverage => {
1884                    output.effective_leverage = resp.effective_leverage.parse().ok()
1885                }
1886                CalcIndex::LeverageRatio => {
1887                    output.leverage_ratio = resp.leverage_ratio.parse().ok()
1888                }
1889                CalcIndex::ConversionRatio => {
1890                    output.conversion_ratio = resp.conversion_ratio.parse().ok()
1891                }
1892                CalcIndex::BalancePoint => output.balance_point = resp.balance_point.parse().ok(),
1893                CalcIndex::OpenInterest => output.open_interest = Some(resp.open_interest),
1894                CalcIndex::Delta => output.delta = resp.delta.parse().ok(),
1895                CalcIndex::Gamma => output.gamma = resp.gamma.parse().ok(),
1896                CalcIndex::Theta => output.theta = resp.theta.parse().ok(),
1897                CalcIndex::Vega => output.vega = resp.vega.parse().ok(),
1898                CalcIndex::Rho => output.rho = resp.rho.parse().ok(),
1899            }
1900        }
1901
1902        output
1903    }
1904}
1905
1906/// Security list category
1907#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
1908pub enum SecurityListCategory {
1909    /// Overnight
1910    Overnight,
1911}
1912
1913impl_serialize_for_enum_string!(SecurityListCategory);
1914
1915/// The basic information of securities
1916#[derive(Debug, Serialize, Deserialize)]
1917pub struct Security {
1918    /// Security code
1919    pub symbol: String,
1920    /// Security name (zh-CN)
1921    pub name_cn: String,
1922    /// Security name (en)
1923    pub name_en: String,
1924    /// Security name (zh-HK)
1925    pub name_hk: String,
1926}
1927
1928/// Quote package detail
1929#[derive(Debug, Clone)]
1930pub struct QuotePackageDetail {
1931    /// Key
1932    pub key: String,
1933    /// Name
1934    pub name: String,
1935    /// Description
1936    pub description: String,
1937    /// Start time
1938    pub start_at: OffsetDateTime,
1939    /// End time
1940    pub end_at: OffsetDateTime,
1941}
1942
1943impl TryFrom<quote::user_quote_level_detail::PackageDetail> for QuotePackageDetail {
1944    type Error = Error;
1945
1946    fn try_from(quote: quote::user_quote_level_detail::PackageDetail) -> Result<Self> {
1947        Ok(Self {
1948            key: quote.key,
1949            name: quote.name,
1950            description: quote.description,
1951            start_at: OffsetDateTime::from_unix_timestamp(quote.start)
1952                .map_err(|err| Error::parse_field_error("start_at", err))?,
1953            end_at: OffsetDateTime::from_unix_timestamp(quote.end)
1954                .map_err(|err| Error::parse_field_error("end_at", err))?,
1955        })
1956    }
1957}
1958
1959/// Trade sessions
1960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1961#[repr(i32)]
1962pub enum TradeSessions {
1963    /// Intraday
1964    Intraday = 0,
1965    /// All
1966    All = 100,
1967}
1968
1969impl TradeSessions {
1970    #[inline]
1971    pub(crate) fn contains(&self, session: TradeSession) -> bool {
1972        match self {
1973            TradeSessions::Intraday => session == TradeSession::Intraday,
1974            TradeSessions::All => true,
1975        }
1976    }
1977}
1978
1979/// Market temperature
1980#[derive(Debug, Clone, Serialize, Deserialize)]
1981pub struct MarketTemperature {
1982    /// Temperature value
1983    pub temperature: i32,
1984    /// Temperature description
1985    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
1986    pub description: String,
1987    /// Market valuation
1988    pub valuation: i32,
1989    /// Market sentiment
1990    pub sentiment: i32,
1991    /// Time
1992    #[serde(
1993        serialize_with = "time::serde::rfc3339::serialize",
1994        deserialize_with = "serde_utils::timestamp::deserialize",
1995        alias = "updated_at"
1996    )]
1997    pub timestamp: OffsetDateTime,
1998}
1999
2000/// Data granularity
2001#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
2002pub enum Granularity {
2003    /// Unknown
2004    Unknown,
2005    /// Daily
2006    #[strum(serialize = "daily")]
2007    Daily,
2008    /// Weekly
2009    #[strum(serialize = "weekly")]
2010    Weekly,
2011    /// Monthly
2012    #[strum(serialize = "monthly")]
2013    Monthly,
2014}
2015
2016/// History market temperature response
2017#[derive(Debug, Clone, Serialize, Deserialize)]
2018pub struct HistoryMarketTemperatureResponse {
2019    /// Granularity
2020    #[serde(rename = "type")]
2021    pub granularity: Granularity,
2022    /// Records
2023    #[serde(rename = "list")]
2024    pub records: Vec<MarketTemperature>,
2025}
2026
2027/// Filing item
2028#[derive(Debug, Clone, Serialize, Deserialize)]
2029pub struct FilingItem {
2030    /// Filing ID
2031    pub id: String,
2032    /// Title
2033    pub title: String,
2034    /// Description
2035    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2036    pub description: String,
2037    /// File name
2038    pub file_name: String,
2039    /// File URLs
2040    pub file_urls: Vec<String>,
2041    /// Published time
2042    #[serde(
2043        rename = "publish_at",
2044        serialize_with = "time::serde::rfc3339::serialize",
2045        deserialize_with = "crate::serde_utils::timestamp::deserialize"
2046    )]
2047    pub published_at: OffsetDateTime,
2048}
2049
2050impl_serde_for_enum_string!(Granularity);
2051impl_default_for_enum_string!(
2052    OptionType,
2053    OptionDirection,
2054    OptionExpiryCycleType,
2055    OptionStandardAttr,
2056    WarrantType,
2057    SecurityBoard,
2058    Granularity
2059);
2060
2061// ── short_positions ───────────────────────────────────────────────
2062
2063/// One short-position data point (unified for US and HK markets).
2064#[derive(Debug, Clone, Serialize, Deserialize)]
2065pub struct ShortPositionsItem {
2066    /// Trading date (RFC 3339, e.g. `"2024-01-15T00:00:00Z"`)
2067    pub timestamp: String,
2068    /// Short ratio (both markets)
2069    pub rate: String,
2070    /// Closing price (both markets)
2071    pub close: String,
2072    /// [US] Number of short shares outstanding
2073    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2074    pub current_shares_short: String,
2075    /// [US] Average daily share volume
2076    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2077    pub avg_daily_share_volume: String,
2078    /// [US] Days to cover ratio
2079    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2080    pub days_to_cover: String,
2081    /// [HK] Short sale amount (HKD)
2082    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2083    pub amount: String,
2084    /// [HK] Short position balance
2085    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2086    pub balance: String,
2087    /// [HK] Cost / closing price
2088    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2089    pub cost: String,
2090}
2091
2092/// Response for [`crate::QuoteContext::short_positions`]
2093#[derive(Debug, Clone, Serialize, Deserialize)]
2094pub struct ShortPositionsResponse {
2095    /// Short position data points
2096    pub data: Vec<ShortPositionsItem>,
2097}
2098
2099// ── option_volume ─────────────────────────────────────────────────
2100
2101/// Response for [`crate::QuoteContext::option_volume`]
2102#[derive(Debug, Clone, Serialize, Deserialize)]
2103pub struct OptionVolumeStats {
2104    /// Underlying security symbol
2105    pub symbol: String,
2106    /// Total call volume
2107    pub call_volume: i64,
2108    /// Total put volume
2109    pub put_volume: i64,
2110}
2111
2112// ── option_volume_daily ───────────────────────────────────────────
2113
2114/// Response for [`crate::QuoteContext::option_volume_daily`]
2115#[derive(Debug, Clone, Serialize, Deserialize)]
2116pub struct OptionVolumeDaily {
2117    /// Underlying security symbol
2118    pub symbol: String,
2119    /// Daily option volume records
2120    pub stats: Vec<OptionVolumeDailyStat>,
2121}
2122
2123/// One day's option volume statistics
2124#[derive(Debug, Clone, Serialize, Deserialize)]
2125pub struct OptionVolumeDailyStat {
2126    /// Underlying security symbol
2127    pub symbol: String,
2128    /// Trading date
2129    pub date: Date,
2130    /// Call volume
2131    pub call_volume: i64,
2132    /// Put volume
2133    pub put_volume: i64,
2134    /// Call open interest
2135    pub call_open_interest: i64,
2136    /// Put open interest
2137    pub put_open_interest: i64,
2138    /// Total options volume (calls + puts)
2139    pub total_volume: i64,
2140    /// Total open interest (calls + puts)
2141    pub total_open_interest: i64,
2142    /// Put/call volume ratio
2143    pub pc_vol: f64,
2144    /// Put/call open interest ratio
2145    pub pc_oi: f64,
2146}
2147
2148// ── short_trades ──────────────────────────────────────────────────
2149
2150/// One short-trade data point (unified for US and HK markets).
2151#[derive(Debug, Clone, Serialize, Deserialize)]
2152pub struct ShortTradesItem {
2153    /// Trading date (RFC 3339)
2154    pub timestamp: String,
2155    /// Short ratio
2156    pub rate: String,
2157    /// Closing price
2158    pub close: String,
2159    /// [US] NYSE short amount
2160    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2161    pub nus_amount: String,
2162    /// [US] NY short amount
2163    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2164    pub ny_amount: String,
2165    /// [US] Total short amount
2166    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2167    pub total_amount: String,
2168    /// [HK] Short sale amount
2169    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2170    pub amount: String,
2171    /// [HK] Short position balance
2172    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2173    pub balance: String,
2174}
2175
2176/// Response for [`crate::QuoteContext::short_trades`]
2177#[derive(Debug, Clone, Serialize, Deserialize)]
2178pub struct ShortTradesResponse {
2179    /// Short trade data points
2180    pub data: Vec<ShortTradesItem>,
2181}
2182
2183// ── pinned mode ───────────────────────────────────────────────────
2184
2185/// Mode for pinning/unpinning watchlist securities
2186#[derive(Debug, Clone, Serialize, Deserialize)]
2187#[serde(rename_all = "lowercase")]
2188pub enum PinnedMode {
2189    /// Pin (add) the securities to the top of the group
2190    Add,
2191    /// Unpin (remove) the securities from the top of the group
2192    Remove,
2193}
2194
2195// ── US-market types
2196// ───────────────────────────────────────────────────────────
2197
2198/// Market overview for a single cryptocurrency.
2199///
2200/// Returned by [`crate::QuoteContext::us_crypto_overview`].
2201#[derive(Debug, Clone, Serialize, Deserialize)]
2202pub struct USCryptoOverview {
2203    /// Full name (e.g. `"Bitcoin"`)
2204    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2205    pub name: String,
2206    /// Ticker symbol (e.g. `"BTC"`)
2207    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2208    pub ticker: String,
2209    /// Pricing currency
2210    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2211    pub currency: String,
2212    /// All-time high price
2213    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2214    pub all_time_high: String,
2215    /// All-time high date
2216    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2217    pub all_time_high_date: String,
2218    /// All-time low price
2219    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2220    pub all_time_low: String,
2221    /// All-time low date
2222    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2223    pub all_time_low_date: String,
2224    /// Listing date
2225    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2226    pub ipo_date: String,
2227    /// Issue price
2228    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2229    pub issue_price: String,
2230    /// Circulating supply
2231    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2232    pub shares: String,
2233    /// User-facing symbol (e.g. `"BTCUSD.BKKT"`)
2234    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2235    pub symbol: String,
2236    /// Base asset code (e.g. `"BTC"`)
2237    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2238    pub base_asset: String,
2239    /// Official website URL
2240    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2241    pub official_web_address: String,
2242    /// Logo image URL
2243    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2244    pub logo: String,
2245    /// In-app wiki URL
2246    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2247    pub wiki_url: String,
2248    /// Multi-language profile / description (JSON string)
2249    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
2250    pub profile: String,
2251}
2252
2253#[cfg(test)]
2254mod tests {
2255    use serde::Deserialize;
2256
2257    use crate::{Market, quote::WatchlistGroup};
2258
2259    #[test]
2260    fn watch_list() {
2261        #[derive(Debug, Deserialize)]
2262        struct Response {
2263            groups: Vec<WatchlistGroup>,
2264        }
2265
2266        let json = r#"
2267        {
2268            "groups": [
2269                {
2270                    "id": "1",
2271                    "name": "Test",
2272                    "securities": [
2273                        {
2274                            "symbol": "AAPL",
2275                            "market": "US",
2276                            "name": "Apple Inc.",
2277                            "watched_price": "150.0",
2278                            "watched_at": "1633036800"
2279                        }
2280                    ]
2281                }
2282            ]
2283        }
2284        "#;
2285
2286        let response: Response = serde_json::from_str(json).unwrap();
2287        assert_eq!(response.groups.len(), 1);
2288        assert_eq!(response.groups[0].id, 1);
2289        assert_eq!(response.groups[0].name, "Test");
2290        assert_eq!(response.groups[0].securities.len(), 1);
2291        assert_eq!(response.groups[0].securities[0].symbol, "AAPL");
2292        assert_eq!(response.groups[0].securities[0].market, Market::US);
2293        assert_eq!(response.groups[0].securities[0].name, "Apple Inc.");
2294        assert_eq!(
2295            response.groups[0].securities[0].watched_price,
2296            Some(decimal!(150.0))
2297        );
2298        assert_eq!(
2299            response.groups[0].securities[0].watched_at.unix_timestamp(),
2300            1633036800
2301        );
2302    }
2303}