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