use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::data::models::{
Bar, News, Orderbook, Quote, Trade, TradeCancel, TradeCorrection, TradingStatus,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Channel {
Trades,
Quotes,
Orderbooks,
Bars,
UpdatedBars,
DailyBars,
Statuses,
Lulds,
News,
Corrections,
CancelErrors,
}
impl Channel {
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Trades => "trades",
Self::Quotes => "quotes",
Self::Orderbooks => "orderbooks",
Self::Bars => "bars",
Self::UpdatedBars => "updatedBars",
Self::DailyBars => "dailyBars",
Self::Statuses => "statuses",
Self::Lulds => "lulds",
Self::News => "news",
Self::Corrections => "corrections",
Self::CancelErrors => "cancelErrors",
}
}
#[must_use]
pub fn from_message_type(message_type: &str) -> Option<Self> {
Some(match message_type {
"t" => Self::Trades,
"q" => Self::Quotes,
"o" => Self::Orderbooks,
"b" => Self::Bars,
"u" => Self::UpdatedBars,
"d" => Self::DailyBars,
"s" => Self::Statuses,
"l" => Self::Lulds,
"n" => Self::News,
"c" => Self::Corrections,
"x" => Self::CancelErrors,
_ => return None,
})
}
#[must_use]
pub const fn is_subscribable(self) -> bool {
!matches!(self, Self::Corrections | Self::CancelErrors)
}
pub const ALL: &'static [Self] = &[
Self::Trades,
Self::Quotes,
Self::Orderbooks,
Self::Bars,
Self::UpdatedBars,
Self::DailyBars,
Self::Statuses,
Self::Lulds,
Self::News,
Self::Corrections,
Self::CancelErrors,
];
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Subscriptions {
#[serde(default)]
pub trades: Vec<String>,
#[serde(default)]
pub quotes: Vec<String>,
#[serde(default)]
pub orderbooks: Vec<String>,
#[serde(default)]
pub bars: Vec<String>,
#[serde(default, rename = "updatedBars")]
pub updated_bars: Vec<String>,
#[serde(default, rename = "dailyBars")]
pub daily_bars: Vec<String>,
#[serde(default)]
pub statuses: Vec<String>,
#[serde(default)]
pub lulds: Vec<String>,
#[serde(default)]
pub news: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StreamError {
#[serde(default)]
pub code: Option<i64>,
#[serde(default, rename = "msg")]
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum StreamMessage {
Trade(Trade),
Quote(Quote),
Orderbook(Orderbook),
Bar(Bar),
UpdatedBar(Bar),
DailyBar(Bar),
TradingStatus(TradingStatus),
News(News),
Correction(TradeCorrection),
CancelError(TradeCancel),
Other {
message_type: String,
raw: Value,
},
Subscription(Subscriptions),
Error(StreamError),
}
impl StreamMessage {
#[must_use]
pub fn symbol(&self) -> Option<&str> {
match self {
Self::Trade(t) => Some(&t.symbol),
Self::Quote(q) => Some(&q.symbol),
Self::Orderbook(o) => Some(&o.symbol),
Self::Bar(b) | Self::UpdatedBar(b) | Self::DailyBar(b) => Some(&b.symbol),
Self::TradingStatus(s) => Some(&s.symbol),
Self::Correction(c) => Some(&c.symbol),
Self::CancelError(c) => Some(&c.symbol),
Self::News(_) | Self::Other { .. } | Self::Subscription(_) | Self::Error(_) => None,
}
}
#[must_use]
pub fn channel(&self) -> Option<Channel> {
Some(match self {
Self::Trade(_) => Channel::Trades,
Self::Quote(_) => Channel::Quotes,
Self::Orderbook(_) => Channel::Orderbooks,
Self::Bar(_) => Channel::Bars,
Self::UpdatedBar(_) => Channel::UpdatedBars,
Self::DailyBar(_) => Channel::DailyBars,
Self::TradingStatus(_) => Channel::Statuses,
Self::News(_) => Channel::News,
Self::Correction(_) => Channel::Corrections,
Self::CancelError(_) => Channel::CancelErrors,
Self::Other { message_type, .. } => Channel::from_message_type(message_type)?,
Self::Subscription(_) | Self::Error(_) => return None,
})
}
#[must_use]
pub fn is_market_data(&self) -> bool {
!matches!(self, Self::Subscription(_) | Self::Error(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_types_map_to_channels() {
assert_eq!(Channel::from_message_type("t"), Some(Channel::Trades));
assert_eq!(Channel::from_message_type("u"), Some(Channel::UpdatedBars));
assert_eq!(Channel::from_message_type("x"), Some(Channel::CancelErrors));
assert_eq!(Channel::from_message_type("subscription"), None);
assert_eq!(Channel::from_message_type("error"), None);
}
#[test]
fn wire_names_are_camel_case_where_alpaca_uses_it() {
assert_eq!(Channel::UpdatedBars.wire_name(), "updatedBars");
assert_eq!(Channel::DailyBars.wire_name(), "dailyBars");
assert_eq!(Channel::CancelErrors.wire_name(), "cancelErrors");
assert_eq!(Channel::Trades.wire_name(), "trades");
}
#[test]
fn corrections_and_cancel_errors_are_not_subscribable() {
assert!(!Channel::Corrections.is_subscribable());
assert!(!Channel::CancelErrors.is_subscribable());
for channel in Channel::ALL {
if !matches!(channel, Channel::Corrections | Channel::CancelErrors) {
assert!(channel.is_subscribable(), "{channel:?}");
}
}
}
#[test]
fn control_frames_are_not_market_data() {
assert!(!StreamMessage::Subscription(Subscriptions::default()).is_market_data());
assert!(
!StreamMessage::Error(StreamError {
code: Some(400),
message: "bad".to_owned(),
})
.is_market_data()
);
}
#[test]
fn subscription_frames_deserialize_with_camel_case_keys() {
let subs: Subscriptions = serde_json::from_str(
r#"{"trades":["AAPL"],"updatedBars":["SPY"],"dailyBars":[],"quotes":["MSFT"]}"#,
)
.unwrap();
assert_eq!(subs.trades, ["AAPL"]);
assert_eq!(subs.updated_bars, ["SPY"]);
assert_eq!(subs.quotes, ["MSFT"]);
assert!(subs.daily_bars.is_empty());
}
}