use rust_decimal::Decimal;
use rust_decimal::serde::float_option as decimal_opt;
use serde::Deserialize;
use strum::{Display, EnumString, FromRepr};
use crate::error::{Error, Result};
use crate::streamer::{Service, subscription::SubscriptionField};
impl SubscriptionField for Field {
const SERVICE: Service = Service::LevelOneEquities;
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
serde_repr::Serialize_repr,
Display,
EnumString,
FromRepr,
)]
#[repr(u8)]
#[strum(serialize_all = "snake_case")]
#[non_exhaustive]
pub enum Field {
Symbol,
BidPrice,
AskPrice,
LastPrice,
BidSize,
AskSize,
AskId,
BidId,
TotalVolume,
LastSize,
HighPrice,
LowPrice,
ClosePrice,
ExchangeId,
Marginable,
Description,
LastId,
OpenPrice,
NetChange,
High52WeekPrice,
Low52WeekPrice,
PeRatio,
AnnualDividendAmount,
DividendYield,
Nav,
ExchangeName,
DividendDate,
RegularMarketQuote,
RegularMarketTrade,
RegularMarketLastPrice,
RegularMarketLastSize,
RegularMarketNetChange,
SecurityStatus,
MarkPrice,
QuoteTime,
TradeTime,
RegularMarketTradeTime,
BidTime,
AskTime,
AskMicId,
BidMicId,
LastMicId,
NetPercentageChange,
RegularMarketPercentageChange,
MarkPriceNetChange,
MarkPricePercentageChange,
HardToBorrowQuantity,
HardToBorrowRate,
HardToBorrow,
Shortable,
PostMarketNetChange,
PostMarketPercentageChange,
}
impl From<Field> for u8 {
fn from(field: Field) -> Self {
field as u8
}
}
impl TryFrom<u8> for Field {
type Error = String;
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
Field::from_repr(value).ok_or_else(|| format!("Invalid field: {}", value))
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
#[serde(default)]
#[non_exhaustive]
pub struct Content {
pub key: String,
pub delayed: bool,
#[serde(rename = "assetMainType")]
pub asset_main_type: Option<String>,
#[serde(rename = "assetSubType")]
pub asset_sub_type: Option<String>,
pub cusip: Option<String>,
pub symbol: Option<String>,
#[serde(with = "decimal_opt")]
pub bid_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub ask_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub last_price: Option<Decimal>,
pub bid_size: Option<u64>,
pub ask_size: Option<u64>,
pub ask_id: Option<String>,
pub bid_id: Option<String>,
pub total_volume: Option<u64>,
pub last_size: Option<u64>,
#[serde(with = "decimal_opt")]
pub high_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub low_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub close_price: Option<Decimal>,
pub exchange_id: Option<String>,
pub marginable: Option<bool>,
pub description: Option<String>,
pub last_id: Option<String>,
#[serde(with = "decimal_opt")]
pub open_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub net_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub high52_week_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub low52_week_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub pe_ratio: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub annual_dividend_amount: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub dividend_yield: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub nav: Option<Decimal>,
pub exchange_name: Option<String>,
pub dividend_date: Option<String>,
pub regular_market_quote: Option<bool>,
pub regular_market_trade: Option<bool>,
#[serde(with = "decimal_opt")]
pub regular_market_last_price: Option<Decimal>,
pub regular_market_last_size: Option<u64>,
#[serde(with = "decimal_opt")]
pub regular_market_net_change: Option<Decimal>,
pub security_status: Option<String>,
#[serde(with = "decimal_opt")]
pub mark_price: Option<Decimal>,
pub quote_time: Option<u64>,
pub trade_time: Option<u64>,
pub regular_market_trade_time: Option<u64>,
pub bid_time: Option<u64>,
pub ask_time: Option<u64>,
pub ask_mic_id: Option<String>,
pub bid_mic_id: Option<String>,
pub last_mic_id: Option<String>,
#[serde(with = "decimal_opt")]
pub net_percentage_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub regular_market_percentage_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub mark_price_net_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub mark_price_percentage_change: Option<Decimal>,
pub hard_to_borrow_quantity: Option<i64>,
#[serde(with = "decimal_opt")]
pub hard_to_borrow_rate: Option<Decimal>,
pub hard_to_borrow: Option<i8>,
pub shortable: Option<i8>,
#[serde(with = "decimal_opt")]
pub post_market_net_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub post_market_percentage_change: Option<Decimal>,
}
impl Content {
pub(crate) fn decode_batch(remapped: serde_json::Value) -> Result<Vec<Self>> {
serde_json::from_value(remapped).map_err(|e| Error::Codec {
context: "LEVELONE_EQUITIES content".to_string(),
reason: e.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::streamer::StreamerRequest;
use crate::streamer::response::{DataContent, parse};
use crate::streamer::{StreamerResponse, SubscriptionCommand};
use rust_decimal_macros::dec;
#[test]
fn parses_level_one_equities_data_into_typed_content() {
let frame = r#"{
"data": [{
"service": "LEVELONE_EQUITIES",
"timestamp": 1714949592301,
"command": "SUBS",
"content": [
{
"key": "SCHW",
"delayed": false,
"assetMainType": "EQUITY",
"assetSubType": "COE",
"cusip": "808513105",
"1": 76.08, "2": 76.49, "3": 76.44,
"4": 3, "5": 1, "8": 5414735, "10": 76.47
},
{
"key": "AAPL",
"delayed": false,
"assetMainType": "EQUITY",
"assetSubType": "COE",
"cusip": "037833100",
"1": 183.75, "2": 183.8, "3": 183.8,
"4": 1, "5": 2, "8": 163224109, "10": 187
}
]
}]
}"#;
let StreamerResponse::Data(data) = parse(frame).unwrap() else {
panic!("expected Data");
};
assert_eq!(data.len(), 1);
let payload = &data[0];
assert_eq!(payload.service, Service::LevelOneEquities);
assert_eq!(payload.timestamp, 1714949592301);
assert_eq!(payload.command, SubscriptionCommand::Subscribe);
let DataContent::LevelOneEquities(items) = &payload.content else {
panic!("expected LevelOneEquities, got {:?}", payload.content);
};
assert_eq!(items.len(), 2);
let schw = &items[0];
assert_eq!(schw.key, "SCHW");
assert!(!schw.delayed);
assert_eq!(schw.cusip.as_deref(), Some("808513105"));
assert_eq!(schw.bid_price, Some(dec!(76.08)));
assert_eq!(schw.ask_price, Some(dec!(76.49)));
assert_eq!(schw.last_price, Some(dec!(76.44)));
assert_eq!(schw.bid_size, Some(3));
assert_eq!(schw.ask_size, Some(1));
assert_eq!(schw.total_volume, Some(5414735));
assert_eq!(schw.high_price, Some(dec!(76.47)));
assert_eq!(schw.low_price, None);
assert_eq!(schw.dividend_yield, None);
let aapl = &items[1];
assert_eq!(aapl.key, "AAPL");
assert_eq!(aapl.bid_price, Some(dec!(183.75)));
assert_eq!(aapl.last_price, Some(dec!(183.8)));
}
#[test]
fn test_serialize_parameters() {
use crate::streamer::subscription::subscribe_parameters;
let value = subscribe_parameters(
vec!["AAPL".to_string()],
vec![Field::Symbol, Field::BidPrice, Field::AskPrice],
);
assert_eq!(value["keys"], "AAPL");
assert_eq!(value["fields"], "0,1,2");
}
#[test]
fn from_subscription_never_panics() {
use crate::streamer::subscription::{Command, Subscription};
let sub = Subscription {
command: Command::Subscribe,
keys: vec!["AAPL".to_string(), "MSFT,with,commas".to_string()],
fields: vec![Field::Symbol, Field::LastPrice],
};
let _request: StreamerRequest = sub.into();
let sub = Subscription::<Field> {
command: Command::Unsubscribe,
keys: vec![],
fields: vec![],
};
let _request: StreamerRequest = sub.into();
}
}