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::LevelOneOptions;
}
#[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,
Description,
BidPrice,
AskPrice,
LastPrice,
HighPrice,
LowPrice,
ClosePrice,
TotalVolume,
OpenInterest,
Volatility,
MoneyIntrinsicValue,
ExpirationYear,
Multiplier,
Digits,
OpenPrice,
BidSize,
AskSize,
LastSize,
NetChange,
StrikePrice,
ContractType,
Underlying,
ExpirationMonth,
Deliverables,
TimeValue,
ExpirationDay,
DaysToExpiration,
Delta,
Gamma,
Theta,
Vega,
Rho,
SecurityStatus,
TheoreticalOptionValue,
UnderlyingPrice,
UvExpirationType,
MarkPrice,
QuoteTime,
TradeTime,
Exchange,
ExchangeName,
LastTradingDay,
SettlementType,
NetPercentChange,
MarkPriceNetChange,
MarkPricePercentChange,
ImpliedYield,
IsPennyPilot,
OptionRoot,
High52WeekPrice,
Low52WeekPrice,
IndicativeAskPrice,
IndicativeBidPrice,
IndicativeQuoteTime,
ExerciseType,
}
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>,
pub description: 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>,
#[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 total_volume: Option<u64>,
pub open_interest: Option<i64>,
#[serde(with = "decimal_opt")]
pub volatility: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub money_intrinsic_value: Option<Decimal>,
pub expiration_year: Option<i32>,
#[serde(with = "decimal_opt")]
pub multiplier: Option<Decimal>,
pub digits: Option<i32>,
#[serde(with = "decimal_opt")]
pub open_price: Option<Decimal>,
pub bid_size: Option<u64>,
pub ask_size: Option<u64>,
pub last_size: Option<u64>,
#[serde(with = "decimal_opt")]
pub net_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub strike_price: Option<Decimal>,
pub contract_type: Option<String>,
pub underlying: Option<String>,
pub expiration_month: Option<i32>,
pub deliverables: Option<String>,
#[serde(with = "decimal_opt")]
pub time_value: Option<Decimal>,
pub expiration_day: Option<i32>,
pub days_to_expiration: Option<i32>,
#[serde(with = "decimal_opt")]
pub delta: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub gamma: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub theta: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub vega: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub rho: Option<Decimal>,
pub security_status: Option<String>,
#[serde(with = "decimal_opt")]
pub theoretical_option_value: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub underlying_price: Option<Decimal>,
pub uv_expiration_type: Option<String>,
#[serde(with = "decimal_opt")]
pub mark_price: Option<Decimal>,
pub quote_time: Option<u64>,
pub trade_time: Option<u64>,
pub exchange: Option<String>,
pub exchange_name: Option<String>,
pub last_trading_day: Option<i64>,
pub settlement_type: Option<String>,
#[serde(with = "decimal_opt")]
pub net_percent_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub mark_price_net_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub mark_price_percent_change: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub implied_yield: Option<Decimal>,
pub is_penny_pilot: Option<bool>,
pub option_root: Option<String>,
#[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 indicative_ask_price: Option<Decimal>,
#[serde(with = "decimal_opt")]
pub indicative_bid_price: Option<Decimal>,
pub indicative_quote_time: Option<u64>,
pub exercise_type: Option<String>,
}
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_OPTIONS content".to_string(),
reason: e.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::streamer::StreamerRequest;
use crate::streamer::StreamerResponse;
use crate::streamer::response::{DataContent, parse};
use crate::streamer::subscription::{Command, Subscription, subscribe_parameters};
use rust_decimal_macros::dec;
#[test]
fn parses_level_one_options_data_into_typed_content() {
let frame = r#"{
"data": [{
"service": "LEVELONE_OPTIONS",
"timestamp": 1714949592301,
"command": "SUBS",
"content": [{
"key": "AAPL 240315C00200000",
"delayed": false,
"assetMainType": "OPTION",
"2": 5.10, "3": 5.20, "4": 5.15,
"8": 12345, "9": 6789,
"20": 200.0, "21": "C", "22": "AAPL",
"27": 7, "28": 0.52, "29": 0.04, "30": -0.08, "31": 0.13,
"37": 5.15,
"48": true
}]
}]
}"#;
let StreamerResponse::Data(data) = parse(frame).unwrap() else {
panic!("expected Data");
};
let payload = &data[0];
assert_eq!(payload.service, Service::LevelOneOptions);
let DataContent::LevelOneOptions(items) = &payload.content else {
panic!("expected LevelOneOptions, got {:?}", payload.content);
};
assert_eq!(items.len(), 1);
let aapl = &items[0];
assert_eq!(aapl.key, "AAPL 240315C00200000");
assert_eq!(aapl.bid_price, Some(dec!(5.10)));
assert_eq!(aapl.ask_price, Some(dec!(5.20)));
assert_eq!(aapl.last_price, Some(dec!(5.15)));
assert_eq!(aapl.total_volume, Some(12345));
assert_eq!(aapl.open_interest, Some(6789));
assert_eq!(aapl.strike_price, Some(dec!(200.0)));
assert_eq!(aapl.contract_type.as_deref(), Some("C"));
assert_eq!(aapl.underlying.as_deref(), Some("AAPL"));
assert_eq!(aapl.days_to_expiration, Some(7));
assert_eq!(aapl.delta, Some(dec!(0.52)));
assert_eq!(aapl.gamma, Some(dec!(0.04)));
assert_eq!(aapl.theta, Some(dec!(-0.08)));
assert_eq!(aapl.vega, Some(dec!(0.13)));
assert_eq!(aapl.mark_price, Some(dec!(5.15)));
assert_eq!(aapl.is_penny_pilot, Some(true));
assert_eq!(aapl.rho, None);
assert_eq!(aapl.implied_yield, None);
}
#[test]
fn fields_serialize_as_numeric_index() {
let value = subscribe_parameters(
vec!["AAPL 240315C00200000".to_string()],
vec![Field::Symbol, Field::BidPrice, Field::Delta, Field::Gamma],
);
assert_eq!(value["keys"], "AAPL 240315C00200000");
assert_eq!(value["fields"], "0,2,28,29");
}
#[test]
fn from_subscription_never_panics() {
let sub = Subscription {
command: Command::Subscribe,
keys: vec!["XYZ 251219C00050000".to_string()],
fields: vec![Field::Symbol, Field::Delta],
};
let _request: StreamerRequest = sub.into();
let sub = Subscription::<Field> {
command: Command::Unsubscribe,
keys: vec![],
fields: vec![],
};
let _request: StreamerRequest = sub.into();
}
#[test]
fn snake_case_field_names_round_trip() {
assert_eq!(Field::High52WeekPrice.to_string(), "high52_week_price");
assert_eq!(Field::Low52WeekPrice.to_string(), "low52_week_price");
assert_eq!(Field::UvExpirationType.to_string(), "uv_expiration_type");
assert_eq!(Field::IsPennyPilot.to_string(), "is_penny_pilot");
assert_eq!(Field::DaysToExpiration.to_string(), "days_to_expiration");
assert_eq!(
Field::MoneyIntrinsicValue.to_string(),
"money_intrinsic_value"
);
}
}