#[cfg(feature = "databento")]
use crate::exchange::databento::DatabentoErrorKind;
use crate::subscription::{SubKind, candle::CandleInterval};
use rustrade_instrument::{exchange::ExchangeId, index::error::IndexError};
use rustrade_integration::{
error::SocketError,
subscription::{SubscriptionId, display_subscription_ids},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
#[non_exhaustive]
pub enum DataError {
#[error("failed to index market data Subscriptions: {0}")]
Index(#[from] IndexError),
#[error("failed to initialise reconnecting MarketStream due to empty subscriptions")]
SubscriptionsEmpty,
#[error("unsupported DynamicStreams Subscription SubKind: {0}")]
UnsupportedSubKind(SubKind),
#[error("initial snapshot missing for: {0}")]
InitialSnapshotMissing(SubscriptionId),
#[error("initial snapshot invalid: {0}")]
InitialSnapshotInvalid(String),
#[error("SocketError: {0}")]
Socket(String),
#[cfg(feature = "databento")]
#[error("Databento {kind} error ({context}): {message}")]
Databento {
kind: DatabentoErrorKind,
context: String,
message: String,
},
#[cfg(feature = "lse")]
#[error("London Strategic Edge {kind} error: {message}")]
Lse {
kind: crate::exchange::lse::error::LseErrorKind,
message: String,
},
#[error("unsupported dynamic Subscription for exchange: {exchange}, kind: {sub_kind}")]
Unsupported {
exchange: ExchangeId,
sub_kind: SubKind,
},
#[error(
"dynamic Subscription for exchange {exchange} needs a subscriber, and none was supplied \
through DynamicSubscribers"
)]
SubscriberRequired { exchange: ExchangeId },
#[error(
"exchange {exchange} needs the `{feature}` cargo feature, which this build does not enable"
)]
FeatureDisabled {
exchange: ExchangeId,
feature: String,
},
#[error("exchange {exchange} does not support candle interval: {interval}")]
UnsupportedInterval {
exchange: ExchangeId,
interval: CandleInterval,
},
#[error(
"\
InvalidSequence: first_update_id {first_update_id} does not follow on from the \
prev_last_update_id {prev_last_update_id} \
"
)]
InvalidSequence {
prev_last_update_id: u64,
first_update_id: u64,
},
#[error(
"subscriptions not acknowledged ({reason}): {}",
display_subscription_ids(unacknowledged)
)]
SubscriptionsUnacknowledged {
reason: String,
unacknowledged: Vec<SubscriptionId>,
},
#[error("stream re-initialisation attempt {attempt} failed: {error}")]
ReinitFailed { attempt: u32, error: Box<DataError> },
}
impl DataError {
#[allow(clippy::match_like_matches_macro)]
pub fn is_terminal(&self) -> bool {
match self {
DataError::InvalidSequence { .. } => true,
_ => false,
}
}
}
impl From<SocketError> for DataError {
fn from(value: SocketError) -> Self {
match value {
SocketError::Unacknowledged {
reason,
unacknowledged,
} => Self::SubscriptionsUnacknowledged {
reason,
unacknowledged,
},
other => Self::Socket(other.to_string()),
}
}
}
#[cfg(feature = "lse")]
impl From<crate::exchange::lse::error::LseError> for DataError {
fn from(value: crate::exchange::lse::error::LseError) -> Self {
Self::Lse {
kind: value.kind(),
message: value.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unacknowledged_batch_keeps_its_subscriptions_through_the_conversion() {
let error = DataError::from(SocketError::Unacknowledged {
reason: "WebSocket stream terminated unexpectedly".to_owned(),
unacknowledged: vec!["l2Book|XYZ:TSLA".into(), "trades|@107".into()],
});
assert_eq!(
error,
DataError::SubscriptionsUnacknowledged {
reason: "WebSocket stream terminated unexpectedly".to_owned(),
unacknowledged: vec!["l2Book|XYZ:TSLA".into(), "trades|@107".into()],
}
);
assert_eq!(
error.to_string(),
"subscriptions not acknowledged (WebSocket stream terminated unexpectedly): \
l2Book|XYZ:TSLA, trades|@107"
);
}
#[test]
fn test_data_error_is_terminal() {
struct TestCase {
input: DataError,
expected: bool,
}
let tests = vec![
TestCase {
input: DataError::InvalidSequence {
prev_last_update_id: 0,
first_update_id: 0,
},
expected: true,
},
TestCase {
input: DataError::from(SocketError::Sink),
expected: false,
},
TestCase {
input: DataError::UnsupportedInterval {
exchange: ExchangeId::HyperliquidPerp,
interval: CandleInterval::Sec1,
},
expected: false,
},
];
for (index, test) in tests.into_iter().enumerate() {
let actual = test.input.is_terminal();
assert_eq!(actual, test.expected, "TC{} failed", index);
}
}
}