use longport_proto::quote::{self, Period, TradeSession, TradeStatus};
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString};
use time::{Date, OffsetDateTime, Time};
use crate::{
quote::{utils::parse_date, SubFlags},
serde_utils, Error, Market, Result,
};
#[derive(Debug, Clone)]
pub struct Subscription {
pub symbol: String,
pub sub_types: SubFlags,
pub candlesticks: Vec<Period>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Depth {
pub position: i32,
pub price: Option<Decimal>,
pub volume: i64,
pub order_num: i64,
}
impl TryFrom<quote::Depth> for Depth {
type Error = Error;
fn try_from(depth: quote::Depth) -> Result<Self> {
Ok(Self {
position: depth.position,
price: depth.price.parse().ok(),
volume: depth.volume,
order_num: depth.order_num,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Brokers {
pub position: i32,
pub broker_ids: Vec<i32>,
}
impl From<quote::Brokers> for Brokers {
fn from(brokers: quote::Brokers) -> Self {
Self {
position: brokers.position,
broker_ids: brokers.broker_ids,
}
}
}
#[derive(Debug, FromPrimitive, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[repr(i32)]
pub enum TradeDirection {
#[num_enum(default)]
Neutral = 0,
Down = 1,
Up = 2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trade {
pub price: Decimal,
pub volume: i64,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub trade_type: String,
pub direction: TradeDirection,
pub trade_session: TradeSession,
}
impl TryFrom<quote::Trade> for Trade {
type Error = Error;
fn try_from(trade: quote::Trade) -> Result<Self> {
Ok(Self {
price: trade.price.parse().unwrap_or_default(),
volume: trade.volume,
timestamp: OffsetDateTime::from_unix_timestamp(trade.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
trade_type: trade.trade_type,
direction: trade.direction.into(),
trade_session: TradeSession::try_from(trade.trade_session).unwrap_or_default(),
})
}
}
bitflags::bitflags! {
#[derive(Debug, Copy, Clone, Serialize,Deserialize)]
pub struct DerivativeType: u8 {
const OPTION = 0x1;
const WARRANT = 0x2;
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display, Serialize, Deserialize)]
#[allow(clippy::upper_case_acronyms)]
pub enum SecurityBoard {
#[strum(disabled)]
Unknown,
USMain,
USPink,
USDJI,
USNSDQ,
USSector,
USOption,
USOptionS,
HKEquity,
HKPreIPO,
HKWarrant,
HKHS,
HKSector,
SHMainConnect,
SHMainNonConnect,
SHSTAR,
CNIX,
CNSector,
SZMainConnect,
SZMainNonConnect,
SZGEMConnect,
SZGEMNonConnect,
SGMain,
STI,
SGSector,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SecurityStaticInfo {
pub symbol: String,
pub name_cn: String,
pub name_en: String,
pub name_hk: String,
pub exchange: String,
pub currency: String,
pub lot_size: i32,
pub total_shares: i64,
pub circulating_shares: i64,
pub hk_shares: i64,
pub eps: Decimal,
pub eps_ttm: Decimal,
pub bps: Decimal,
pub dividend_yield: Decimal,
pub stock_derivatives: DerivativeType,
pub board: SecurityBoard,
}
impl TryFrom<quote::StaticInfo> for SecurityStaticInfo {
type Error = Error;
fn try_from(resp: quote::StaticInfo) -> Result<Self> {
Ok(SecurityStaticInfo {
symbol: resp.symbol,
name_cn: resp.name_cn,
name_en: resp.name_en,
name_hk: resp.name_hk,
exchange: resp.exchange,
currency: resp.currency,
lot_size: resp.lot_size,
total_shares: resp.total_shares,
circulating_shares: resp.circulating_shares,
hk_shares: resp.hk_shares,
eps: resp.eps.parse().unwrap_or_default(),
eps_ttm: resp.eps_ttm.parse().unwrap_or_default(),
bps: resp.bps.parse().unwrap_or_default(),
dividend_yield: resp.dividend_yield.parse().unwrap_or_default(),
stock_derivatives: resp.stock_derivatives.into_iter().fold(
DerivativeType::empty(),
|acc, value| match value {
1 => acc | DerivativeType::OPTION,
2 => acc | DerivativeType::WARRANT,
_ => acc,
},
),
board: resp.board.parse().unwrap_or(SecurityBoard::Unknown),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealtimeQuote {
pub symbol: String,
pub last_done: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub trade_status: TradeStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrePostQuote {
pub last_done: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub high: Decimal,
pub low: Decimal,
pub prev_close: Decimal,
}
impl TryFrom<quote::PrePostQuote> for PrePostQuote {
type Error = Error;
fn try_from(quote: quote::PrePostQuote) -> Result<Self> {
Ok(Self {
last_done: quote.last_done.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
volume: quote.volume,
turnover: quote.turnover.parse().unwrap_or_default(),
high: quote.high.parse().unwrap_or_default(),
low: quote.low.parse().unwrap_or_default(),
prev_close: quote.prev_close.parse().unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub trade_status: TradeStatus,
pub pre_market_quote: Option<PrePostQuote>,
pub post_market_quote: Option<PrePostQuote>,
pub overnight_quote: Option<PrePostQuote>,
}
impl TryFrom<quote::SecurityQuote> for SecurityQuote {
type Error = Error;
fn try_from(quote: quote::SecurityQuote) -> Result<Self> {
Ok(Self {
symbol: quote.symbol,
last_done: quote.last_done.parse().unwrap_or_default(),
prev_close: quote.prev_close.parse().unwrap_or_default(),
open: quote.open.parse().unwrap_or_default(),
high: quote.high.parse().unwrap_or_default(),
low: quote.low.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
volume: quote.volume,
turnover: quote.turnover.parse().unwrap_or_default(),
trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
pre_market_quote: quote.pre_market_quote.map(TryInto::try_into).transpose()?,
post_market_quote: quote.post_market_quote.map(TryInto::try_into).transpose()?,
overnight_quote: quote.over_night_quote.map(TryInto::try_into).transpose()?,
})
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
pub enum OptionType {
#[strum(disabled)]
Unknown,
#[strum(serialize = "A")]
American,
#[strum(serialize = "U")]
Europe,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Serialize, Deserialize)]
pub enum OptionDirection {
#[strum(disabled)]
Unknown,
#[strum(serialize = "P")]
Put,
#[strum(serialize = "C")]
Call,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptionQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub trade_status: TradeStatus,
pub implied_volatility: Decimal,
pub open_interest: i64,
pub expiry_date: Date,
pub strike_price: Decimal,
pub contract_multiplier: Decimal,
pub contract_type: OptionType,
pub contract_size: Decimal,
pub direction: OptionDirection,
pub historical_volatility: Decimal,
pub underlying_symbol: String,
}
impl TryFrom<quote::OptionQuote> for OptionQuote {
type Error = Error;
fn try_from(quote: quote::OptionQuote) -> Result<Self> {
let option_extend = quote.option_extend.unwrap_or_default();
Ok(Self {
symbol: quote.symbol,
last_done: quote.last_done.parse().unwrap_or_default(),
prev_close: quote.prev_close.parse().unwrap_or_default(),
open: quote.open.parse().unwrap_or_default(),
high: quote.high.parse().unwrap_or_default(),
low: quote.low.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
volume: quote.volume,
turnover: quote.turnover.parse().unwrap_or_default(),
trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
implied_volatility: option_extend.implied_volatility.parse().unwrap_or_default(),
open_interest: option_extend.open_interest,
expiry_date: parse_date(&option_extend.expiry_date)
.map_err(|err| Error::parse_field_error("expiry_date", err))?,
strike_price: option_extend.strike_price.parse().unwrap_or_default(),
contract_multiplier: option_extend
.contract_multiplier
.parse()
.unwrap_or_default(),
contract_type: option_extend.contract_type.parse().unwrap_or_default(),
contract_size: option_extend.contract_size.parse().unwrap_or_default(),
direction: option_extend.contract_type.parse().unwrap_or_default(),
historical_volatility: option_extend
.historical_volatility
.parse()
.unwrap_or_default(),
underlying_symbol: option_extend.underlying_symbol,
})
}
}
#[derive(
Debug,
Copy,
Clone,
Hash,
Eq,
PartialEq,
EnumString,
IntoPrimitive,
TryFromPrimitive,
Serialize,
Deserialize,
)]
#[repr(i32)]
pub enum WarrantType {
#[strum(disabled)]
Unknown = -1,
Call = 0,
Put = 1,
Bull = 2,
Bear = 3,
Inline = 4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WarrantQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub trade_status: TradeStatus,
pub implied_volatility: Decimal,
pub expiry_date: Date,
pub last_trade_date: Date,
pub outstanding_ratio: Decimal,
pub outstanding_quantity: i64,
pub conversion_ratio: Decimal,
pub category: WarrantType,
pub strike_price: Decimal,
pub upper_strike_price: Decimal,
pub lower_strike_price: Decimal,
pub call_price: Decimal,
pub underlying_symbol: String,
}
impl TryFrom<quote::WarrantQuote> for WarrantQuote {
type Error = Error;
fn try_from(quote: quote::WarrantQuote) -> Result<Self> {
let warrant_extend = quote.warrant_extend.unwrap_or_default();
Ok(Self {
symbol: quote.symbol,
last_done: quote.last_done.parse().unwrap_or_default(),
prev_close: quote.prev_close.parse().unwrap_or_default(),
open: quote.open.parse().unwrap_or_default(),
high: quote.high.parse().unwrap_or_default(),
low: quote.low.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(quote.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
volume: quote.volume,
turnover: quote.turnover.parse().unwrap_or_default(),
trade_status: TradeStatus::try_from(quote.trade_status).unwrap_or_default(),
implied_volatility: warrant_extend
.implied_volatility
.parse()
.unwrap_or_default(),
expiry_date: parse_date(&warrant_extend.expiry_date)
.map_err(|err| Error::parse_field_error("expiry_date", err))?,
last_trade_date: parse_date(&warrant_extend.last_trade_date)
.map_err(|err| Error::parse_field_error("last_trade_date", err))?,
outstanding_ratio: warrant_extend.outstanding_ratio.parse().unwrap_or_default(),
outstanding_quantity: warrant_extend.outstanding_qty,
conversion_ratio: warrant_extend.conversion_ratio.parse().unwrap_or_default(),
category: warrant_extend.category.parse().unwrap_or_default(),
strike_price: warrant_extend.strike_price.parse().unwrap_or_default(),
upper_strike_price: warrant_extend
.upper_strike_price
.parse()
.unwrap_or_default(),
lower_strike_price: warrant_extend
.lower_strike_price
.parse()
.unwrap_or_default(),
call_price: warrant_extend.call_price.parse().unwrap_or_default(),
underlying_symbol: warrant_extend.underlying_symbol,
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecurityDepth {
pub asks: Vec<Depth>,
pub bids: Vec<Depth>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecurityBrokers {
pub ask_brokers: Vec<Brokers>,
pub bid_brokers: Vec<Brokers>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticipantInfo {
pub broker_ids: Vec<i32>,
pub name_cn: String,
pub name_en: String,
pub name_hk: String,
}
impl From<quote::ParticipantInfo> for ParticipantInfo {
fn from(info: quote::ParticipantInfo) -> Self {
Self {
broker_ids: info.broker_ids,
name_cn: info.participant_name_cn,
name_en: info.participant_name_en,
name_hk: info.participant_name_hk,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntradayLine {
pub price: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
pub volume: i64,
pub turnover: Decimal,
pub avg_price: Decimal,
}
impl TryFrom<quote::Line> for IntradayLine {
type Error = Error;
fn try_from(value: quote::Line) -> Result<Self> {
Ok(Self {
price: value.price.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
volume: value.volume,
turnover: value.turnover.parse().unwrap_or_default(),
avg_price: value.avg_price.parse().unwrap_or_default(),
})
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Candlestick {
pub close: Decimal,
pub open: Decimal,
pub low: Decimal,
pub high: Decimal,
pub volume: i64,
pub turnover: Decimal,
#[serde(with = "serde_utils::timestamp")]
pub timestamp: OffsetDateTime,
}
impl TryFrom<quote::Candlestick> for Candlestick {
type Error = Error;
fn try_from(value: quote::Candlestick) -> Result<Self> {
Ok(Self {
close: value.close.parse().unwrap_or_default(),
open: value.open.parse().unwrap_or_default(),
low: value.low.parse().unwrap_or_default(),
high: value.high.parse().unwrap_or_default(),
volume: value.volume,
turnover: value.turnover.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
})
}
}
impl From<longport_candlesticks::Candlestick> for Candlestick {
#[inline]
fn from(candlestick: longport_candlesticks::Candlestick) -> Self {
Self {
close: candlestick.close,
open: candlestick.open,
low: candlestick.low,
high: candlestick.high,
volume: candlestick.volume,
turnover: candlestick.turnover,
timestamp: candlestick.time,
}
}
}
impl From<Candlestick> for longport_candlesticks::Candlestick {
#[inline]
fn from(candlestick: Candlestick) -> Self {
Self {
time: candlestick.timestamp,
open: candlestick.open,
high: candlestick.high,
low: candlestick.low,
close: candlestick.close,
volume: candlestick.volume,
turnover: candlestick.turnover,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrikePriceInfo {
pub price: Decimal,
pub call_symbol: String,
pub put_symbol: String,
pub standard: bool,
}
impl TryFrom<quote::StrikePriceInfo> for StrikePriceInfo {
type Error = Error;
fn try_from(value: quote::StrikePriceInfo) -> Result<Self> {
Ok(Self {
price: value.price.parse().unwrap_or_default(),
call_symbol: value.call_symbol,
put_symbol: value.put_symbol,
standard: value.standard,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssuerInfo {
pub issuer_id: i32,
pub name_cn: String,
pub name_en: String,
pub name_hk: String,
}
impl From<quote::IssuerInfo> for IssuerInfo {
fn from(info: quote::IssuerInfo) -> Self {
Self {
issuer_id: info.id,
name_cn: info.name_cn,
name_en: info.name_en,
name_hk: info.name_hk,
}
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
#[repr(i32)]
pub enum SortOrderType {
Ascending = 0,
Descending = 1,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
#[repr(i32)]
pub enum WarrantSortBy {
LastDone = 0,
ChangeRate = 1,
ChangeValue = 2,
Volume = 3,
Turnover = 4,
ExpiryDate = 5,
StrikePrice = 6,
UpperStrikePrice = 7,
LowerStrikePrice = 8,
OutstandingQuantity = 9,
OutstandingRatio = 10,
Premium = 11,
ItmOtm = 12,
ImpliedVolatility = 13,
Delta = 14,
CallPrice = 15,
ToCallPrice = 16,
EffectiveLeverage = 17,
LeverageRatio = 18,
ConversionRatio = 19,
BalancePoint = 20,
Status = 21,
}
#[allow(non_camel_case_types)]
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
#[repr(i32)]
pub enum FilterWarrantExpiryDate {
LT_3 = 1,
Between_3_6 = 2,
Between_6_12 = 3,
GT_12 = 4,
}
#[allow(non_camel_case_types)]
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive)]
#[repr(i32)]
pub enum FilterWarrantInOutBoundsType {
In = 1,
Out = 2,
}
#[derive(
Debug, Copy, Clone, Hash, Eq, PartialEq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
)]
#[repr(i32)]
pub enum WarrantStatus {
Suspend = 2,
PrepareList = 3,
Normal = 4,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WarrantInfo {
pub symbol: String,
pub warrant_type: WarrantType,
pub name: String,
pub last_done: Decimal,
pub change_rate: Decimal,
pub change_value: Decimal,
pub volume: i64,
pub turnover: Decimal,
pub expiry_date: Date,
pub strike_price: Option<Decimal>,
pub upper_strike_price: Option<Decimal>,
pub lower_strike_price: Option<Decimal>,
pub outstanding_qty: i64,
pub outstanding_ratio: Decimal,
pub premium: Decimal,
pub itm_otm: Option<Decimal>,
pub implied_volatility: Option<Decimal>,
pub delta: Option<Decimal>,
pub call_price: Option<Decimal>,
pub to_call_price: Option<Decimal>,
pub effective_leverage: Option<Decimal>,
pub leverage_ratio: Decimal,
pub conversion_ratio: Option<Decimal>,
pub balance_point: Option<Decimal>,
pub status: WarrantStatus,
}
impl TryFrom<quote::FilterWarrant> for WarrantInfo {
type Error = Error;
fn try_from(info: quote::FilterWarrant) -> Result<Self> {
let r#type = WarrantType::try_from(info.r#type)
.map_err(|err| Error::parse_field_error("type", err))?;
match r#type {
WarrantType::Unknown => unreachable!(),
WarrantType::Call | WarrantType::Put => Ok(Self {
symbol: info.symbol,
warrant_type: r#type,
name: info.name,
last_done: info.last_done.parse().unwrap_or_default(),
change_rate: info.change_rate.parse().unwrap_or_default(),
change_value: info.change_val.parse().unwrap_or_default(),
volume: info.volume,
turnover: info.turnover.parse().unwrap_or_default(),
expiry_date: parse_date(&info.expiry_date)
.map_err(|err| Error::parse_field_error("expiry_date", err))?,
strike_price: Some(info.last_done.parse().unwrap_or_default()),
upper_strike_price: None,
lower_strike_price: None,
outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
premium: info.premium.parse().unwrap_or_default(),
itm_otm: Some(info.last_done.parse().unwrap_or_default()),
implied_volatility: Some(info.last_done.parse().unwrap_or_default()),
delta: Some(info.last_done.parse().unwrap_or_default()),
call_price: None,
to_call_price: None,
effective_leverage: Some(info.last_done.parse().unwrap_or_default()),
leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
conversion_ratio: Some(info.last_done.parse().unwrap_or_default()),
balance_point: Some(info.last_done.parse().unwrap_or_default()),
status: WarrantStatus::try_from(info.status)
.map_err(|err| Error::parse_field_error("state", err))?,
}),
WarrantType::Bull | WarrantType::Bear => Ok(Self {
symbol: info.symbol,
warrant_type: r#type,
name: info.name,
last_done: info.last_done.parse().unwrap_or_default(),
change_rate: info.change_rate.parse().unwrap_or_default(),
change_value: info.change_val.parse().unwrap_or_default(),
volume: info.volume,
turnover: info.turnover.parse().unwrap_or_default(),
expiry_date: parse_date(&info.expiry_date)
.map_err(|err| Error::parse_field_error("expiry_date", err))?,
strike_price: Some(info.last_done.parse().unwrap_or_default()),
upper_strike_price: None,
lower_strike_price: None,
outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
premium: info.premium.parse().unwrap_or_default(),
itm_otm: Some(info.last_done.parse().unwrap_or_default()),
implied_volatility: None,
delta: None,
call_price: Some(info.call_price.parse().unwrap_or_default()),
to_call_price: Some(info.to_call_price.parse().unwrap_or_default()),
effective_leverage: None,
leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
conversion_ratio: Some(info.last_done.parse().unwrap_or_default()),
balance_point: Some(info.last_done.parse().unwrap_or_default()),
status: WarrantStatus::try_from(info.status)
.map_err(|err| Error::parse_field_error("state", err))?,
}),
WarrantType::Inline => Ok(Self {
symbol: info.symbol,
warrant_type: r#type,
name: info.name,
last_done: info.last_done.parse().unwrap_or_default(),
change_rate: info.change_rate.parse().unwrap_or_default(),
change_value: info.change_val.parse().unwrap_or_default(),
volume: info.volume,
turnover: info.turnover.parse().unwrap_or_default(),
expiry_date: parse_date(&info.expiry_date)
.map_err(|err| Error::parse_field_error("expiry_date", err))?,
strike_price: None,
upper_strike_price: Some(info.upper_strike_price.parse().unwrap_or_default()),
lower_strike_price: Some(info.lower_strike_price.parse().unwrap_or_default()),
outstanding_qty: info.outstanding_qty.parse().unwrap_or_default(),
outstanding_ratio: info.outstanding_ratio.parse().unwrap_or_default(),
premium: info.premium.parse().unwrap_or_default(),
itm_otm: None,
implied_volatility: None,
delta: None,
call_price: None,
to_call_price: None,
effective_leverage: None,
leverage_ratio: info.leverage_ratio.parse().unwrap_or_default(),
conversion_ratio: None,
balance_point: None,
status: WarrantStatus::try_from(info.status)
.map_err(|err| Error::parse_field_error("state", err))?,
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradingSessionInfo {
pub begin_time: Time,
pub end_time: Time,
pub trade_session: TradeSession,
}
impl TryFrom<quote::TradePeriod> for TradingSessionInfo {
type Error = Error;
fn try_from(value: quote::TradePeriod) -> Result<Self> {
#[inline]
fn parse_time(value: i32) -> ::std::result::Result<Time, time::error::ComponentRange> {
Time::from_hms(((value / 100) % 100) as u8, (value % 100) as u8, 0)
}
Ok(Self {
begin_time: parse_time(value.beg_time)
.map_err(|err| Error::parse_field_error("beg_time", err))?,
end_time: parse_time(value.end_time)
.map_err(|err| Error::parse_field_error("end_time", err))?,
trade_session: TradeSession::try_from(value.trade_session).unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketTradingSession {
pub market: Market,
pub trade_sessions: Vec<TradingSessionInfo>,
}
impl TryFrom<quote::MarketTradePeriod> for MarketTradingSession {
type Error = Error;
fn try_from(value: quote::MarketTradePeriod) -> Result<Self> {
Ok(Self {
market: value.market.parse().unwrap_or_default(),
trade_sessions: value
.trade_session
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>>>()?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketTradingDays {
pub trading_days: Vec<Date>,
pub half_trading_days: Vec<Date>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapitalFlowLine {
pub inflow: Decimal,
pub timestamp: OffsetDateTime,
}
impl TryFrom<quote::capital_flow_intraday_response::CapitalFlowLine> for CapitalFlowLine {
type Error = Error;
fn try_from(value: quote::capital_flow_intraday_response::CapitalFlowLine) -> Result<Self> {
Ok(Self {
inflow: value.inflow.parse().unwrap_or_default(),
timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CapitalDistribution {
pub large: Decimal,
pub medium: Decimal,
pub small: Decimal,
}
impl TryFrom<quote::capital_distribution_response::CapitalDistribution> for CapitalDistribution {
type Error = Error;
fn try_from(value: quote::capital_distribution_response::CapitalDistribution) -> Result<Self> {
Ok(Self {
large: value.large.parse().unwrap_or_default(),
medium: value.medium.parse().unwrap_or_default(),
small: value.small.parse().unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapitalDistributionResponse {
pub timestamp: OffsetDateTime,
pub capital_in: CapitalDistribution,
pub capital_out: CapitalDistribution,
}
impl TryFrom<quote::CapitalDistributionResponse> for CapitalDistributionResponse {
type Error = Error;
fn try_from(value: quote::CapitalDistributionResponse) -> Result<Self> {
Ok(Self {
timestamp: OffsetDateTime::from_unix_timestamp(value.timestamp)
.map_err(|err| Error::parse_field_error("timestamp", err))?,
capital_in: value
.capital_in
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default(),
capital_out: value
.capital_out
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistSecurity {
pub symbol: String,
pub market: Market,
pub name: String,
#[serde(with = "serde_utils::decimal_opt_empty_is_none")]
pub watched_price: Option<Decimal>,
#[serde(with = "serde_utils::timestamp")]
pub watched_at: OffsetDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchlistGroup {
#[serde(with = "serde_utils::int64_str")]
pub id: i64,
pub name: String,
pub securities: Vec<WatchlistSecurity>,
}
impl_default_for_enum_string!(OptionType, OptionDirection, WarrantType, SecurityBoard);
#[derive(Debug, Clone)]
pub struct RequestCreateWatchlistGroup {
pub name: String,
pub securities: Option<Vec<String>>,
}
impl RequestCreateWatchlistGroup {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
securities: None,
}
}
pub fn securities<I, T>(self, securities: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
Self {
securities: Some(securities.into_iter().map(Into::into).collect()),
..self
}
}
}
#[derive(Debug, Copy, Clone, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SecuritiesUpdateMode {
Add,
Remove,
#[default]
Replace,
}
#[derive(Debug, Clone)]
pub struct RequestUpdateWatchlistGroup {
pub id: i64,
pub name: Option<String>,
pub securities: Option<Vec<String>>,
pub mode: SecuritiesUpdateMode,
}
impl RequestUpdateWatchlistGroup {
#[inline]
pub fn new(id: i64) -> Self {
Self {
id,
name: None,
securities: None,
mode: SecuritiesUpdateMode::default(),
}
}
pub fn name(self, name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..self
}
}
pub fn securities<I, T>(self, securities: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<String>,
{
Self {
securities: Some(securities.into_iter().map(Into::into).collect()),
..self
}
}
pub fn mode(self, mode: SecuritiesUpdateMode) -> Self {
Self { mode, ..self }
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CalcIndex {
LastDone,
ChangeValue,
ChangeRate,
Volume,
Turnover,
YtdChangeRate,
TurnoverRate,
TotalMarketValue,
CapitalFlow,
Amplitude,
VolumeRatio,
PeTtmRatio,
PbRatio,
DividendRatioTtm,
FiveDayChangeRate,
TenDayChangeRate,
HalfYearChangeRate,
FiveMinutesChangeRate,
ExpiryDate,
StrikePrice,
UpperStrikePrice,
LowerStrikePrice,
OutstandingQty,
OutstandingRatio,
Premium,
ItmOtm,
ImpliedVolatility,
WarrantDelta,
CallPrice,
ToCallPrice,
EffectiveLeverage,
LeverageRatio,
ConversionRatio,
BalancePoint,
OpenInterest,
Delta,
Gamma,
Theta,
Vega,
Rho,
}
impl From<CalcIndex> for longport_proto::quote::CalcIndex {
fn from(value: CalcIndex) -> Self {
use longport_proto::quote::CalcIndex::*;
match value {
CalcIndex::LastDone => CalcindexLastDone,
CalcIndex::ChangeValue => CalcindexChangeVal,
CalcIndex::ChangeRate => CalcindexChangeRate,
CalcIndex::Volume => CalcindexVolume,
CalcIndex::Turnover => CalcindexTurnover,
CalcIndex::YtdChangeRate => CalcindexYtdChangeRate,
CalcIndex::TurnoverRate => CalcindexTurnoverRate,
CalcIndex::TotalMarketValue => CalcindexTotalMarketValue,
CalcIndex::CapitalFlow => CalcindexCapitalFlow,
CalcIndex::Amplitude => CalcindexAmplitude,
CalcIndex::VolumeRatio => CalcindexVolumeRatio,
CalcIndex::PeTtmRatio => CalcindexPeTtmRatio,
CalcIndex::PbRatio => CalcindexPbRatio,
CalcIndex::DividendRatioTtm => CalcindexDividendRatioTtm,
CalcIndex::FiveDayChangeRate => CalcindexFiveDayChangeRate,
CalcIndex::TenDayChangeRate => CalcindexTenDayChangeRate,
CalcIndex::HalfYearChangeRate => CalcindexHalfYearChangeRate,
CalcIndex::FiveMinutesChangeRate => CalcindexFiveMinutesChangeRate,
CalcIndex::ExpiryDate => CalcindexExpiryDate,
CalcIndex::StrikePrice => CalcindexStrikePrice,
CalcIndex::UpperStrikePrice => CalcindexUpperStrikePrice,
CalcIndex::LowerStrikePrice => CalcindexLowerStrikePrice,
CalcIndex::OutstandingQty => CalcindexOutstandingQty,
CalcIndex::OutstandingRatio => CalcindexOutstandingRatio,
CalcIndex::Premium => CalcindexPremium,
CalcIndex::ItmOtm => CalcindexItmOtm,
CalcIndex::ImpliedVolatility => CalcindexImpliedVolatility,
CalcIndex::WarrantDelta => CalcindexWarrantDelta,
CalcIndex::CallPrice => CalcindexCallPrice,
CalcIndex::ToCallPrice => CalcindexToCallPrice,
CalcIndex::EffectiveLeverage => CalcindexEffectiveLeverage,
CalcIndex::LeverageRatio => CalcindexLeverageRatio,
CalcIndex::ConversionRatio => CalcindexConversionRatio,
CalcIndex::BalancePoint => CalcindexBalancePoint,
CalcIndex::OpenInterest => CalcindexOpenInterest,
CalcIndex::Delta => CalcindexDelta,
CalcIndex::Gamma => CalcindexGamma,
CalcIndex::Theta => CalcindexTheta,
CalcIndex::Vega => CalcindexVega,
CalcIndex::Rho => CalcindexRho,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityCalcIndex {
pub symbol: String,
pub last_done: Option<Decimal>,
pub change_value: Option<Decimal>,
pub change_rate: Option<Decimal>,
pub volume: Option<i64>,
pub turnover: Option<Decimal>,
pub ytd_change_rate: Option<Decimal>,
pub turnover_rate: Option<Decimal>,
pub total_market_value: Option<Decimal>,
pub capital_flow: Option<Decimal>,
pub amplitude: Option<Decimal>,
pub volume_ratio: Option<Decimal>,
pub pe_ttm_ratio: Option<Decimal>,
pub pb_ratio: Option<Decimal>,
pub dividend_ratio_ttm: Option<Decimal>,
pub five_day_change_rate: Option<Decimal>,
pub ten_day_change_rate: Option<Decimal>,
pub half_year_change_rate: Option<Decimal>,
pub five_minutes_change_rate: Option<Decimal>,
pub expiry_date: Option<Date>,
pub strike_price: Option<Decimal>,
pub upper_strike_price: Option<Decimal>,
pub lower_strike_price: Option<Decimal>,
pub outstanding_qty: Option<i64>,
pub outstanding_ratio: Option<Decimal>,
pub premium: Option<Decimal>,
pub itm_otm: Option<Decimal>,
pub implied_volatility: Option<Decimal>,
pub warrant_delta: Option<Decimal>,
pub call_price: Option<Decimal>,
pub to_call_price: Option<Decimal>,
pub effective_leverage: Option<Decimal>,
pub leverage_ratio: Option<Decimal>,
pub conversion_ratio: Option<Decimal>,
pub balance_point: Option<Decimal>,
pub open_interest: Option<i64>,
pub delta: Option<Decimal>,
pub gamma: Option<Decimal>,
pub theta: Option<Decimal>,
pub vega: Option<Decimal>,
pub rho: Option<Decimal>,
}
impl SecurityCalcIndex {
pub(crate) fn from_proto(
resp: longport_proto::quote::SecurityCalcIndex,
indexes: &[CalcIndex],
) -> Self {
let mut output = SecurityCalcIndex {
symbol: resp.symbol,
last_done: None,
change_value: None,
change_rate: None,
volume: None,
turnover: None,
ytd_change_rate: None,
turnover_rate: None,
total_market_value: None,
capital_flow: None,
amplitude: None,
volume_ratio: None,
pe_ttm_ratio: None,
pb_ratio: None,
dividend_ratio_ttm: None,
five_day_change_rate: None,
ten_day_change_rate: None,
half_year_change_rate: None,
five_minutes_change_rate: None,
expiry_date: None,
strike_price: None,
upper_strike_price: None,
lower_strike_price: None,
outstanding_qty: None,
outstanding_ratio: None,
premium: None,
itm_otm: None,
implied_volatility: None,
warrant_delta: None,
call_price: None,
to_call_price: None,
effective_leverage: None,
leverage_ratio: None,
conversion_ratio: None,
balance_point: None,
open_interest: None,
delta: None,
gamma: None,
theta: None,
vega: None,
rho: None,
};
for index in indexes {
match index {
CalcIndex::LastDone => output.last_done = resp.last_done.parse().ok(),
CalcIndex::ChangeValue => output.change_value = resp.change_val.parse().ok(),
CalcIndex::ChangeRate => output.change_rate = resp.change_rate.parse().ok(),
CalcIndex::Volume => output.volume = Some(resp.volume),
CalcIndex::Turnover => output.turnover = resp.turnover.parse().ok(),
CalcIndex::YtdChangeRate => {
output.ytd_change_rate = resp.ytd_change_rate.parse().ok()
}
CalcIndex::TurnoverRate => output.turnover_rate = resp.turnover_rate.parse().ok(),
CalcIndex::TotalMarketValue => {
output.total_market_value = resp.total_market_value.parse().ok()
}
CalcIndex::CapitalFlow => output.capital_flow = resp.capital_flow.parse().ok(),
CalcIndex::Amplitude => output.amplitude = resp.amplitude.parse().ok(),
CalcIndex::VolumeRatio => output.volume_ratio = resp.volume_ratio.parse().ok(),
CalcIndex::PeTtmRatio => output.pe_ttm_ratio = resp.pe_ttm_ratio.parse().ok(),
CalcIndex::PbRatio => output.pb_ratio = resp.pb_ratio.parse().ok(),
CalcIndex::DividendRatioTtm => {
output.dividend_ratio_ttm = resp.dividend_ratio_ttm.parse().ok()
}
CalcIndex::FiveDayChangeRate => {
output.five_day_change_rate = resp.five_day_change_rate.parse().ok()
}
CalcIndex::TenDayChangeRate => {
output.ten_day_change_rate = resp.ten_day_change_rate.parse().ok()
}
CalcIndex::HalfYearChangeRate => {
output.half_year_change_rate = resp.half_year_change_rate.parse().ok()
}
CalcIndex::FiveMinutesChangeRate => {
output.five_minutes_change_rate = resp.five_minutes_change_rate.parse().ok()
}
CalcIndex::ExpiryDate => output.expiry_date = parse_date(&resp.expiry_date).ok(),
CalcIndex::StrikePrice => output.strike_price = resp.strike_price.parse().ok(),
CalcIndex::UpperStrikePrice => {
output.upper_strike_price = resp.upper_strike_price.parse().ok()
}
CalcIndex::LowerStrikePrice => {
output.lower_strike_price = resp.lower_strike_price.parse().ok()
}
CalcIndex::OutstandingQty => output.outstanding_qty = Some(resp.outstanding_qty),
CalcIndex::OutstandingRatio => {
output.outstanding_ratio = resp.outstanding_ratio.parse().ok()
}
CalcIndex::Premium => output.premium = resp.premium.parse().ok(),
CalcIndex::ItmOtm => output.itm_otm = resp.itm_otm.parse().ok(),
CalcIndex::ImpliedVolatility => {
output.implied_volatility = resp.implied_volatility.parse().ok()
}
CalcIndex::WarrantDelta => output.warrant_delta = resp.warrant_delta.parse().ok(),
CalcIndex::CallPrice => output.call_price = resp.call_price.parse().ok(),
CalcIndex::ToCallPrice => output.to_call_price = resp.to_call_price.parse().ok(),
CalcIndex::EffectiveLeverage => {
output.effective_leverage = resp.effective_leverage.parse().ok()
}
CalcIndex::LeverageRatio => {
output.leverage_ratio = resp.leverage_ratio.parse().ok()
}
CalcIndex::ConversionRatio => {
output.conversion_ratio = resp.conversion_ratio.parse().ok()
}
CalcIndex::BalancePoint => output.balance_point = resp.balance_point.parse().ok(),
CalcIndex::OpenInterest => output.open_interest = Some(resp.open_interest),
CalcIndex::Delta => output.delta = resp.delta.parse().ok(),
CalcIndex::Gamma => output.gamma = resp.gamma.parse().ok(),
CalcIndex::Theta => output.theta = resp.theta.parse().ok(),
CalcIndex::Vega => output.vega = resp.vega.parse().ok(),
CalcIndex::Rho => output.rho = resp.rho.parse().ok(),
}
}
output
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
pub enum SecurityListCategory {
Overnight,
}
impl_serialize_for_enum_string!(SecurityListCategory);
#[derive(Debug, Serialize, Deserialize)]
pub struct Security {
pub symbol: String,
pub name_cn: String,
pub name_en: String,
pub name_hk: String,
}
#[derive(Debug, Clone)]
pub struct QuotePackageDetail {
pub key: String,
pub name: String,
pub description: String,
pub start_at: OffsetDateTime,
pub end_at: OffsetDateTime,
}
impl TryFrom<quote::user_quote_level_detail::PackageDetail> for QuotePackageDetail {
type Error = Error;
fn try_from(quote: quote::user_quote_level_detail::PackageDetail) -> Result<Self> {
Ok(Self {
key: quote.key,
name: quote.name,
description: quote.description,
start_at: OffsetDateTime::from_unix_timestamp(quote.start)
.map_err(|err| Error::parse_field_error("start_at", err))?,
end_at: OffsetDateTime::from_unix_timestamp(quote.end)
.map_err(|err| Error::parse_field_error("end_at", err))?,
})
}
}