use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EventKind {
Quote,
Trade,
TradeEth,
Greeks,
Candle,
Summary,
TimeAndSale,
Profile,
Underlying,
TheoPrice,
Series,
}
impl EventKind {
pub const ALL: [EventKind; 11] = [
EventKind::Quote,
EventKind::TradeEth,
EventKind::Trade,
EventKind::Greeks,
EventKind::Candle,
EventKind::Summary,
EventKind::TimeAndSale,
EventKind::Profile,
EventKind::Underlying,
EventKind::TheoPrice,
EventKind::Series,
];
pub fn wire_name(&self) -> &'static str {
match self {
EventKind::Quote => "Quote",
EventKind::Trade => "Trade",
EventKind::TradeEth => "TradeETH",
EventKind::Greeks => "Greeks",
EventKind::Candle => "Candle",
EventKind::Summary => "Summary",
EventKind::TimeAndSale => "TimeAndSale",
EventKind::Profile => "Profile",
EventKind::Underlying => "Underlying",
EventKind::TheoPrice => "TheoPrice",
EventKind::Series => "Series",
}
}
pub fn needs_a_period(&self) -> bool {
matches!(self, EventKind::Candle)
}
}
impl std::fmt::Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.wire_name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum CandleUnit {
Seconds,
Minutes,
Hours,
Days,
Weeks,
Months,
}
impl CandleUnit {
pub fn as_str(&self) -> &'static str {
match self {
CandleUnit::Seconds => "s",
CandleUnit::Minutes => "m",
CandleUnit::Hours => "h",
CandleUnit::Days => "d",
CandleUnit::Weeks => "w",
CandleUnit::Months => "mo",
}
}
}
impl std::fmt::Display for CandleUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct CandlePeriod {
count: std::num::NonZeroU32,
unit: CandleUnit,
}
impl CandlePeriod {
pub fn new(count: u32, unit: CandleUnit) -> crate::TastyResult<Self> {
let count = std::num::NonZeroU32::new(count).ok_or_else(|| {
crate::TastyTradeError::Precondition(
"a candle period of zero is not a period; the venue accepts the suffix \
and then delivers nothing, which looks exactly like a quiet market"
.to_string(),
)
})?;
Ok(Self { count, unit })
}
pub fn seconds(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Seconds)
}
pub fn minutes(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Minutes)
}
pub fn hours(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Hours)
}
pub fn days(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Days)
}
pub fn weeks(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Weeks)
}
pub fn months(count: u32) -> crate::TastyResult<Self> {
Self::new(count, CandleUnit::Months)
}
pub fn count(&self) -> u32 {
self.count.get()
}
pub fn unit(&self) -> CandleUnit {
self.unit
}
pub fn suffix(&self) -> String {
format!("{{={}{}}}", self.count, self.unit.as_str())
}
pub fn streamer_symbol(&self, symbol: &str) -> String {
format!("{symbol}{}", self.suffix())
}
}
impl std::fmt::Display for CandlePeriod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.suffix())
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfQuoteT {
pub time: i64,
pub sequence: i32,
pub time_nanos: i32,
pub bid_time: i64,
pub bid_exchange_code: i16,
pub bid_price: f64,
pub ask_price: f64,
pub bid_size: i64,
pub ask_time: i64,
pub ask_size: i64,
pub ask_exchange_code: i16,
pub scope: i32,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfTradeT {
pub time: i64,
pub sequence: i32,
pub time_nanos: i32,
pub exchange_code: i16,
pub price: f64,
pub size: i64,
pub tick: i32,
pub change: f64,
pub day_id: i32,
pub day_volume: f64,
pub day_turnover: f64,
pub raw_flags: i32,
pub direction: i32,
pub is_eth: i32,
pub scope: i32,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfGreeksT {
pub event_flags: i32,
pub index: i64,
pub time: i64,
pub price: f64,
pub volatility: f64,
pub delta: f64,
pub gamma: f64,
pub theta: f64,
pub rho: f64,
pub vega: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfCandleT {
pub event_time: i64,
pub event_flags: i64,
pub index: i64,
pub time: i64,
pub sequence: i64,
pub count: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub vwap: f64,
pub bid_volume: f64,
pub ask_volume: f64,
pub imp_volatility: f64,
pub open_interest: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfSummaryT {
pub event_time: i64,
pub day_id: i64,
pub day_open_price: f64,
pub day_high_price: f64,
pub day_low_price: f64,
pub day_close_price: f64,
pub day_close_price_type: String,
pub prev_day_id: i64,
pub prev_day_close_price: f64,
pub prev_day_close_price_type: String,
pub prev_day_volume: f64,
pub open_interest: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfTimeAndSaleT {
pub event_time: i64,
pub event_flags: i64,
pub index: i64,
pub time: i64,
pub time_nano_part: i64,
pub sequence: i64,
pub exchange_code: String,
pub price: f64,
pub size: f64,
pub bid_price: f64,
pub ask_price: f64,
pub exchange_sale_conditions: String,
pub trade_through_exempt: String,
pub aggressor_side: String,
pub spread_leg: bool,
pub extended_trading_hours: bool,
pub valid_tick: bool,
pub sale_type: String,
pub buyer: String,
pub seller: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfProfileT {
pub event_time: i64,
pub description: String,
pub short_sale_restriction: String,
pub trading_status: String,
pub status_reason: String,
pub halt_start_time: i64,
pub halt_end_time: i64,
pub high_limit_price: f64,
pub low_limit_price: f64,
pub high_52_week_price: f64,
pub low_52_week_price: f64,
pub beta: f64,
pub earnings_per_share: f64,
pub dividend_frequency: f64,
pub ex_dividend_amount: f64,
pub ex_dividend_day_id: i64,
pub shares: f64,
pub free_float: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfUnderlyingT {
pub event_time: i64,
pub event_flags: i64,
pub index: i64,
pub time: i64,
pub sequence: i64,
pub volatility: f64,
pub front_volatility: f64,
pub back_volatility: f64,
pub call_volume: f64,
pub put_volume: f64,
pub put_call_ratio: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfTheoPriceT {
pub event_time: i64,
pub event_flags: i64,
pub index: i64,
pub time: i64,
pub sequence: i64,
pub price: f64,
pub underlying_price: f64,
pub delta: f64,
pub gamma: f64,
pub dividend: f64,
pub interest: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfTradeEthT {
pub event_time: i64,
pub time: i64,
pub time_nano_part: i64,
pub sequence: i64,
pub exchange_code: String,
pub price: f64,
pub change: f64,
pub size: f64,
pub day_id: i64,
pub day_volume: f64,
pub day_turnover: f64,
pub tick_direction: String,
pub extended_trading_hours: bool,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct DxfSeriesT {
pub event_time: i64,
pub event_flags: i64,
pub index: i64,
pub time: i64,
pub sequence: i64,
pub expiration: i64,
pub volatility: f64,
pub call_volume: f64,
pub put_volume: f64,
pub put_call_ratio: f64,
pub forward_price: f64,
pub dividend: f64,
pub interest: f64,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub enum EventData {
Quote(DxfQuoteT),
Trade(DxfTradeT),
TradeEth(Box<DxfTradeEthT>),
Greeks(DxfGreeksT),
Candle(Box<DxfCandleT>),
Summary(Box<DxfSummaryT>),
TimeAndSale(Box<DxfTimeAndSaleT>),
Profile(Box<DxfProfileT>),
Underlying(Box<DxfUnderlyingT>),
TheoPrice(Box<DxfTheoPriceT>),
Series(Box<DxfSeriesT>),
}
impl EventData {
pub fn kind(&self) -> EventKind {
match self {
EventData::Quote(_) => EventKind::Quote,
EventData::Trade(_) => EventKind::Trade,
EventData::TradeEth(_) => EventKind::TradeEth,
EventData::Greeks(_) => EventKind::Greeks,
EventData::Candle(_) => EventKind::Candle,
EventData::Summary(_) => EventKind::Summary,
EventData::TimeAndSale(_) => EventKind::TimeAndSale,
EventData::Profile(_) => EventKind::Profile,
EventData::Underlying(_) => EventKind::Underlying,
EventData::TheoPrice(_) => EventKind::TheoPrice,
EventData::Series(_) => EventKind::Series,
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct Event {
pub sym: String,
pub data: EventData,
}
impl Event {
pub fn new_quote(symbol: String, quote: DxfQuoteT) -> Self {
Self {
sym: symbol,
data: EventData::Quote(quote),
}
}
pub fn new_trade(symbol: String, trade: DxfTradeT) -> Self {
Self {
sym: symbol,
data: EventData::Trade(trade),
}
}
pub fn new_greeks(symbol: String, greeks: DxfGreeksT) -> Self {
Self {
sym: symbol,
data: EventData::Greeks(greeks),
}
}
}
impl Default for DxfQuoteT {
fn default() -> Self {
Self {
time: 0,
sequence: 0,
time_nanos: 0,
bid_time: 0,
bid_exchange_code: 0,
bid_price: 0.0,
ask_price: 0.0,
bid_size: 0,
ask_time: 0,
ask_size: 0,
ask_exchange_code: 0,
scope: 0,
}
}
}
impl Default for DxfTradeT {
fn default() -> Self {
Self {
time: 0,
sequence: 0,
time_nanos: 0,
exchange_code: 0,
price: 0.0,
size: 0,
tick: 0,
change: 0.0,
day_id: 0,
day_volume: 0.0,
day_turnover: 0.0,
raw_flags: 0,
direction: 0,
is_eth: 0,
scope: 0,
}
}
}
impl Default for DxfGreeksT {
fn default() -> Self {
Self {
event_flags: 0,
index: 0,
time: 0,
price: 0.0,
volatility: 0.0,
delta: 0.0,
gamma: 0.0,
theta: 0.0,
rho: 0.0,
vega: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_event_kind_has_its_wire_name() {
assert_eq!(EventKind::ALL.len(), 11);
let names: Vec<&str> = EventKind::ALL.iter().map(EventKind::wire_name).collect();
assert_eq!(
names,
[
"Quote",
"TradeETH",
"Trade",
"Greeks",
"Candle",
"Summary",
"TimeAndSale",
"Profile",
"Underlying",
"TheoPrice",
"Series",
]
);
for kind in EventKind::ALL {
assert_eq!(kind.needs_a_period(), kind == EventKind::Candle, "{kind}");
}
}
#[test]
fn a_period_renders_the_suffix_the_feed_expects() {
let cases = [
(CandlePeriod::seconds(15), "{=15s}"),
(CandlePeriod::minutes(5), "{=5m}"),
(CandlePeriod::hours(1), "{=1h}"),
(CandlePeriod::days(1), "{=1d}"),
(CandlePeriod::weeks(2), "{=2w}"),
(CandlePeriod::months(3), "{=3mo}"),
];
for (period, expected) in cases {
let period = period.expect("a positive count is a period");
assert_eq!(period.suffix(), expected);
assert_eq!(period.to_string(), expected);
assert_eq!(period.streamer_symbol("AAPL"), format!("AAPL{expected}"));
}
}
#[test]
fn a_zero_period_is_refused_and_cannot_be_built_another_way() {
for period in [
CandlePeriod::seconds(0),
CandlePeriod::minutes(0),
CandlePeriod::hours(0),
CandlePeriod::days(0),
CandlePeriod::weeks(0),
CandlePeriod::months(0),
] {
let error = period.expect_err("zero is not a period");
assert!(
matches!(error, crate::TastyTradeError::Precondition(_)),
"{error:?}"
);
}
for unit in [
CandleUnit::Seconds,
CandleUnit::Minutes,
CandleUnit::Hours,
CandleUnit::Days,
CandleUnit::Weeks,
CandleUnit::Months,
] {
assert!(CandlePeriod::new(0, unit).is_err(), "{unit}");
let period = CandlePeriod::new(1, unit).expect("one is a period");
assert_eq!(period.count(), 1);
assert_eq!(period.unit(), unit);
}
assert!(
!CandlePeriod::minutes(5)
.expect("a period")
.suffix()
.contains("=0")
);
}
#[test]
fn two_periods_of_one_underlying_are_different_symbols() {
let five = CandlePeriod::minutes(5).expect("a period");
let hour = CandlePeriod::hours(1).expect("a period");
assert_ne!(
five.streamer_symbol("AAPL"),
hour.streamer_symbol("AAPL"),
"the period has to be part of the symbol"
);
}
#[test]
fn event_data_reports_its_own_kind() {
assert_eq!(
EventData::Quote(DxfQuoteT::default()).kind(),
EventKind::Quote
);
assert_eq!(
EventData::Trade(DxfTradeT::default()).kind(),
EventKind::Trade
);
assert_eq!(
EventData::Greeks(DxfGreeksT::default()).kind(),
EventKind::Greeks
);
}
#[test]
fn test_dxf_quote_t_default() {
let quote = DxfQuoteT::default();
assert_eq!(quote.time, 0);
assert_eq!(quote.sequence, 0);
assert_eq!(quote.bid_price, 0.0);
assert_eq!(quote.ask_price, 0.0);
assert_eq!(quote.bid_size, 0);
assert_eq!(quote.ask_size, 0);
}
#[test]
fn test_dxf_trade_t_default() {
let trade = DxfTradeT::default();
assert_eq!(trade.time, 0);
assert_eq!(trade.price, 0.0);
assert_eq!(trade.size, 0);
assert_eq!(trade.exchange_code, 0);
assert_eq!(trade.day_volume, 0.0);
}
#[test]
fn test_dxf_greeks_t_default() {
let greeks = DxfGreeksT::default();
assert_eq!(greeks.event_flags, 0);
assert_eq!(greeks.delta, 0.0);
assert_eq!(greeks.gamma, 0.0);
assert_eq!(greeks.theta, 0.0);
assert_eq!(greeks.vega, 0.0);
assert_eq!(greeks.rho, 0.0);
}
#[test]
fn test_event_new_quote() {
let quote = DxfQuoteT {
bid_price: 100.0,
ask_price: 101.0,
bid_size: 100,
ask_size: 200,
..Default::default()
};
let event = Event::new_quote("AAPL".to_string(), quote);
assert_eq!(event.sym, "AAPL");
match event.data {
EventData::Quote(q) => {
assert_eq!(q.bid_price, 100.0);
assert_eq!(q.ask_price, 101.0);
assert_eq!(q.bid_size, 100);
assert_eq!(q.ask_size, 200);
}
_ => panic!("Expected Quote event data"),
}
}
#[test]
fn test_event_new_trade() {
let trade = DxfTradeT {
price: 150.50,
size: 1000,
exchange_code: 1,
..Default::default()
};
let event = Event::new_trade("MSFT".to_string(), trade);
assert_eq!(event.sym, "MSFT");
match event.data {
EventData::Trade(t) => {
assert_eq!(t.price, 150.50);
assert_eq!(t.size, 1000);
assert_eq!(t.exchange_code, 1);
}
_ => panic!("Expected Trade event data"),
}
}
#[test]
fn test_event_new_greeks() {
let greeks = DxfGreeksT {
delta: 0.5,
gamma: 0.1,
theta: -0.05,
vega: 0.2,
rho: 0.03,
volatility: 0.25,
..Default::default()
};
let event = Event::new_greeks("AAPL240920C00150000".to_string(), greeks);
assert_eq!(event.sym, "AAPL240920C00150000");
match event.data {
EventData::Greeks(g) => {
assert_eq!(g.delta, 0.5);
assert_eq!(g.gamma, 0.1);
assert_eq!(g.theta, -0.05);
assert_eq!(g.vega, 0.2);
assert_eq!(g.rho, 0.03);
assert_eq!(g.volatility, 0.25);
}
_ => panic!("Expected Greeks event data"),
}
}
#[test]
fn test_serialization() {
let quote = DxfQuoteT {
bid_price: 100.0,
ask_price: 101.0,
..Default::default()
};
let serialized = serde_json::to_string("e).unwrap();
assert!(serialized.contains("100.0"));
assert!(serialized.contains("101.0"));
let deserialized: DxfQuoteT = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.bid_price, 100.0);
assert_eq!(deserialized.ask_price, 101.0);
}
#[test]
fn test_event_data_enum() {
let quote_data = EventData::Quote(DxfQuoteT::default());
let trade_data = EventData::Trade(DxfTradeT::default());
let greeks_data = EventData::Greeks(DxfGreeksT::default());
match quote_data {
EventData::Quote(_) => {} _ => panic!("Expected Quote variant"),
}
match trade_data {
EventData::Trade(_) => {} _ => panic!("Expected Trade variant"),
}
match greeks_data {
EventData::Greeks(_) => {} _ => panic!("Expected Greeks variant"),
}
}
#[test]
fn test_clone_and_debug() {
let original_quote = DxfQuoteT {
bid_price: 50.0,
ask_price: 51.0,
..Default::default()
};
let cloned_quote = original_quote.clone();
assert_eq!(original_quote.bid_price, cloned_quote.bid_price);
assert_eq!(original_quote.ask_price, cloned_quote.ask_price);
let debug_str = format!("{:?}", original_quote);
assert!(debug_str.contains("50.0"));
}
#[test]
fn test_event_serialization() {
let event = Event::new_quote("TEST".to_string(), DxfQuoteT::default());
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("TEST"));
assert!(serialized.contains("Quote"));
let deserialized: Event = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.sym, "TEST");
matches!(deserialized.data, EventData::Quote(_));
}
}