use longbridge_proto::quote::{self, Period, TradeSession, TradeStatus};
use num_enum::{FromPrimitive, IntoPrimitive};
use rust_decimal::Decimal;
use serde::Deserialize;
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)]
pub struct Depth {
pub position: i32,
pub price: 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().unwrap_or_default(),
volume: depth.volume,
order_num: depth.order_num,
})
}
}
#[derive(Debug, Clone)]
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)]
#[repr(i32)]
pub enum TradeDirection {
#[num_enum(default)]
Neutral = 0,
Down = 1,
Up = 2,
}
#[derive(Debug, Clone)]
pub struct Trade {
pub price: Decimal,
pub volume: i64,
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::from_i32(trade.trade_session).unwrap_or_default(),
})
}
}
bitflags::bitflags! {
pub struct DerivativeType: u8 {
const OPTION = 0x1;
const WARRANT = 0x2;
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString, Display)]
#[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)]
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)]
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)]
pub struct PrePostQuote {
pub last_done: Decimal,
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)]
pub struct SecurityQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
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>,
}
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::from_i32(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()?,
})
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString)]
pub enum OptionType {
#[strum(disabled)]
Unknown,
#[strum(serialize = "A")]
American,
#[strum(serialize = "U")]
Europe,
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, EnumString)]
pub enum OptionDirection {
#[strum(disabled)]
Unknown,
#[strum(serialize = "P")]
Put,
#[strum(serialize = "C")]
Call,
}
#[derive(Debug, Clone)]
pub struct OptionQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
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::from_i32(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)]
#[repr(i32)]
pub enum WarrantType {
#[strum(disabled)]
Unknown = -1,
Call = 0,
Put = 1,
Bull = 2,
Bear = 3,
Inline = 4,
}
#[derive(Debug, Clone)]
pub struct WarrantQuote {
pub symbol: String,
pub last_done: Decimal,
pub prev_close: Decimal,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
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::from_i32(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)]
pub struct SecurityDepth {
pub asks: Vec<Depth>,
pub bids: Vec<Depth>,
}
#[derive(Debug, Clone, Default)]
pub struct SecurityBrokers {
pub ask_brokers: Vec<Brokers>,
pub bid_brokers: Vec<Brokers>,
}
#[derive(Debug, Clone)]
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)]
pub struct IntradayLine {
pub price: Decimal,
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)]
pub struct Candlestick {
pub close: Decimal,
pub open: Decimal,
pub low: Decimal,
pub high: Decimal,
pub volume: i64,
pub turnover: Decimal,
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<longbridge_candlesticks::Candlestick> for Candlestick {
#[inline]
fn from(candlestick: longbridge_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 longbridge_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)]
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)]
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, Clone)]
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::from_i32(value.trade_session).unwrap_or_default(),
})
}
}
#[derive(Debug, Clone)]
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)]
pub struct MarketTradingDays {
pub trading_days: Vec<Date>,
pub half_trading_days: Vec<Date>,
}
#[derive(Debug, Clone)]
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)]
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)]
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, 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, 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);