use rustrade_instrument::{
asset::{AssetIndex, name::AssetNameExchange},
exchange::ExchangeId,
instrument::{InstrumentIndex, name::InstrumentNameExchange},
};
use rustrade_integration::error::SocketError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type UnindexedClientError = ClientError<AssetNameExchange, InstrumentNameExchange>;
pub type UnindexedApiError = ApiError<AssetNameExchange, InstrumentNameExchange>;
pub type UnindexedOrderError = OrderError<AssetNameExchange, InstrumentNameExchange>;
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
pub enum ClientError<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
#[error("Connectivity: {0}")]
Connectivity(#[from] ConnectivityError),
#[error("API: {0}")]
Api(#[from] ApiError<AssetKey, InstrumentKey>),
#[error("task failed: {0}")]
TaskFailed(String),
#[error("internal error: {0}")]
Internal(String),
#[error("activity pagination truncated at {limit} pages — data may be incomplete")]
Truncated {
limit: usize,
},
#[error("open orders snapshot truncated at {limit} results — data may be incomplete")]
TruncatedSnapshot {
limit: usize,
},
}
impl<AssetKey, InstrumentKey> ClientError<AssetKey, InstrumentKey> {
pub fn is_transient(&self) -> bool {
match self {
Self::Connectivity(e) => e.is_transient(),
Self::Api(ApiError::RateLimit) => true,
Self::Api(_) => false,
Self::TaskFailed(_) => false,
Self::Internal(_) => false,
Self::Truncated { .. } => false,
Self::TruncatedSnapshot { .. } => false,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
pub enum ConnectivityError {
#[error("Exchange offline: {0}")]
ExchangeOffline(ExchangeId),
#[error("ExecutionRequest timed out")]
Timeout,
#[error("{0}")]
Socket(String),
}
impl From<SocketError> for ConnectivityError {
fn from(value: SocketError) -> Self {
Self::Socket(value.to_string())
}
}
impl ConnectivityError {
pub fn is_transient(&self) -> bool {
match self {
Self::ExchangeOffline(_) => true,
Self::Timeout => true,
Self::Socket(_) => true,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
pub enum ApiError<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
#[error("asset {0} invalid: {1}")]
AssetInvalid(AssetKey, String),
#[error("instrument {0} invalid: {1}")]
InstrumentInvalid(InstrumentKey, String),
#[error("rate limit exceeded")]
RateLimit,
#[error("authentication failed: {0}")]
Unauthenticated(String),
#[error("asset {0} balance insufficient: {1}")]
BalanceInsufficient(AssetKey, String),
#[error("order rejected: {0}")]
OrderRejected(String),
#[error("order already cancelled")]
OrderAlreadyCancelled,
#[error("order already fully filled")]
OrderAlreadyFullyFilled,
}
#[non_exhaustive]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
pub enum OrderError<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
#[error("connectivity: {0}")]
Connectivity(#[from] ConnectivityError),
#[error("order rejected: {0}")]
Rejected(#[from] ApiError<AssetKey, InstrumentKey>),
#[error("unsupported order type: {0}")]
UnsupportedOrderType(String),
}
impl<AssetKey, InstrumentKey> OrderError<AssetKey, InstrumentKey> {
pub fn is_transient(&self) -> bool {
match self {
Self::Connectivity(e) => e.is_transient(),
Self::Rejected(ApiError::RateLimit) => true,
Self::Rejected(_) => false,
Self::UnsupportedOrderType(_) => false,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Error)]
pub enum KeyError {
#[error("ExchangeId: {0}")]
ExchangeId(String),
#[error("AssetKey: {0}")]
AssetKey(String),
#[error("InstrumentKey: {0}")]
InstrumentKey(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connectivity_error_is_transient() {
assert!(ConnectivityError::Timeout.is_transient());
assert!(ConnectivityError::Socket("connection refused".into()).is_transient());
assert!(ConnectivityError::ExchangeOffline(ExchangeId::BinanceSpot).is_transient());
}
#[test]
fn test_client_error_is_transient_connectivity() {
let err: ClientError = ClientError::Connectivity(ConnectivityError::Timeout);
assert!(err.is_transient());
let err: ClientError = ClientError::Connectivity(ConnectivityError::Socket("err".into()));
assert!(err.is_transient());
}
#[test]
fn test_client_error_is_transient_rate_limit() {
let err: ClientError = ClientError::Api(ApiError::RateLimit);
assert!(err.is_transient());
}
#[test]
fn test_client_error_not_transient_api_errors() {
let err: ClientError =
ClientError::Api(ApiError::AssetInvalid(AssetIndex(0), "bad".into()));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError =
ClientError::Api(ApiError::BalanceInsufficient(AssetIndex(0), "low".into()));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError = ClientError::Api(ApiError::InstrumentInvalid(
InstrumentIndex(0),
"bad".into(),
));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError = ClientError::Api(ApiError::OrderRejected("rejected".into()));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError = ClientError::Api(ApiError::OrderAlreadyCancelled);
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError = ClientError::Api(ApiError::OrderAlreadyFullyFilled);
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError =
ClientError::Api(ApiError::Unauthenticated("invalid signature".into()));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
}
#[test]
fn test_client_error_not_transient_task_failed() {
let err: ClientError = ClientError::TaskFailed("task panicked".into());
assert!(!err.is_transient());
}
#[test]
fn test_client_error_not_transient_internal() {
let err: ClientError = ClientError::Internal("unknown error".into());
assert!(!err.is_transient());
}
#[test]
fn test_client_error_not_transient_truncated() {
let err: ClientError = ClientError::Truncated { limit: 100 };
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: ClientError = ClientError::TruncatedSnapshot { limit: 500 };
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
}
#[test]
fn test_client_error_is_transient_exchange_offline() {
let err: ClientError =
ClientError::Connectivity(ConnectivityError::ExchangeOffline(ExchangeId::BinanceSpot));
assert!(err.is_transient(), "expected transient for {:?}", err);
}
#[test]
fn test_order_error_is_transient_connectivity() {
let err: UnindexedOrderError = OrderError::Connectivity(ConnectivityError::Timeout);
assert!(err.is_transient(), "expected transient for {:?}", err);
let err: UnindexedOrderError =
OrderError::Connectivity(ConnectivityError::Socket("connection reset".into()));
assert!(err.is_transient(), "expected transient for {:?}", err);
let err: UnindexedOrderError =
OrderError::Connectivity(ConnectivityError::ExchangeOffline(ExchangeId::BinanceSpot));
assert!(err.is_transient(), "expected transient for {:?}", err);
}
#[test]
fn test_order_error_is_transient_rate_limit() {
let err: UnindexedOrderError = OrderError::Rejected(ApiError::RateLimit);
assert!(err.is_transient(), "expected transient for {:?}", err);
}
#[test]
fn test_order_error_not_transient_api_errors() {
let err: UnindexedOrderError =
OrderError::Rejected(ApiError::OrderRejected("price out of range".into()));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: UnindexedOrderError = OrderError::Rejected(ApiError::OrderAlreadyCancelled);
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
let err: UnindexedOrderError = OrderError::Rejected(ApiError::BalanceInsufficient(
AssetNameExchange::from("BTC"),
"insufficient".into(),
));
assert!(!err.is_transient(), "expected non-transient for {:?}", err);
}
}