use std::fmt;
#[derive(Debug)]
pub enum IbError {
ConnectionFailed(String),
ConnectionReset,
MarketData {
code: i32,
message: String,
},
OrderRejected {
code: i32,
message: String,
rejection_json: Option<String>,
},
FarmDisconnect {
code: i32,
message: String,
},
ContractResolution(String),
CompetingSession,
Timeout(String),
Other(String),
}
impl fmt::Display for IbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IbError::ConnectionFailed(msg) => write!(f, "connection failed: {msg}"),
IbError::ConnectionReset => write!(f, "connection reset"),
IbError::MarketData { code, message } => {
write!(f, "market data error {code}: {message}")
}
IbError::OrderRejected {
code,
message,
rejection_json,
} => {
if let Some(json) = rejection_json {
write!(f, "order rejection {code}: {message} [{json}]")
} else {
write!(f, "order rejection {code}: {message}")
}
}
IbError::FarmDisconnect { code, message } => {
write!(f, "farm disconnect {code}: {message}")
}
IbError::ContractResolution(msg) => write!(f, "contract resolution failed: {msg}"),
IbError::CompetingSession => {
write!(f, "empty market data — competing session (error 10197)")
}
IbError::Timeout(msg) => write!(f, "timeout: {msg}"),
IbError::Other(msg) => write!(f, "{msg}"),
}
}
}
impl std::error::Error for IbError {}
pub fn classify_notice(code: i32, message: &str, rejection_json: &str) -> IbError {
match code {
10197 => IbError::CompetingSession,
c if (10000..=10999).contains(&c) => IbError::MarketData {
code,
message: message.to_string(),
},
2107 => IbError::FarmDisconnect {
code,
message: message.to_string(),
},
c if (200..=299).contains(&c) => {
let json = if rejection_json.is_empty() {
None
} else {
Some(rejection_json.to_string())
};
IbError::OrderRejected {
code,
message: message.to_string(),
rejection_json: json,
}
}
502 | 504 | 506 => IbError::ConnectionFailed(format!("IB error {code}: {message}")),
507 | 100 => IbError::ConnectionReset,
_ => IbError::Other(format!("IB error {code}: {message}")),
}
}
impl From<ibapi::Error> for IbError {
fn from(err: ibapi::Error) -> Self {
match err {
ibapi::Error::ConnectionFailed => {
IbError::ConnectionFailed("ibapi connection failed".into())
}
ibapi::Error::ConnectionReset => IbError::ConnectionReset,
ibapi::Error::Notice(notice) => classify_notice(
notice.code,
¬ice.message,
¬ice.advanced_order_reject_json,
),
ibapi::Error::Io(io) => IbError::from(io),
ibapi::Error::Shutdown => IbError::ConnectionReset,
other => IbError::Other(format!("{other}")),
}
}
}
impl From<std::io::Error> for IbError {
fn from(err: std::io::Error) -> Self {
match err.kind() {
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::BrokenPipe => IbError::ConnectionReset,
std::io::ErrorKind::TimedOut => IbError::Timeout(err.to_string()),
other => IbError::ConnectionFailed(format!("IO error ({other:?}): {err}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connection_failed_message() {
let e = IbError::ConnectionFailed("timeout".into());
assert_eq!(e.to_string(), "connection failed: timeout");
}
#[test]
fn connection_reset_display() {
let e = IbError::ConnectionReset;
assert_eq!(e.to_string(), "connection reset");
}
#[test]
fn market_data_contains_code_and_message() {
let e = IbError::MarketData {
code: 10197,
message: "No market data".into(),
};
let s = e.to_string();
assert!(s.contains("10197"));
assert!(s.contains("No market data"));
}
#[test]
fn order_rejected_contains_code() {
let e = IbError::OrderRejected {
code: 201,
message: "Order rejected".into(),
rejection_json: Some("{\"reason\":\"bad\"}".into()),
};
let s = e.to_string();
assert!(s.contains("201"));
assert!(s.contains("Order rejected"));
}
#[test]
fn order_rejected_no_json() {
let e = IbError::OrderRejected {
code: 202,
message: "cancelled".into(),
rejection_json: None,
};
assert!(!e.to_string().is_empty());
}
#[test]
fn farm_disconnect_display() {
let e = IbError::FarmDisconnect {
code: 2107,
message: "HMDS data farm lost".into(),
};
let s = e.to_string();
assert!(s.contains("2107"));
}
#[test]
fn contract_resolution_message() {
let e = IbError::ContractResolution("SPY: no conid".into());
assert_eq!(
e.to_string(),
"contract resolution failed: SPY: no conid"
);
}
#[test]
fn competing_session_display() {
let e = IbError::CompetingSession;
assert_eq!(
e.to_string(),
"empty market data — competing session (error 10197)"
);
}
#[test]
fn timeout_message() {
let e = IbError::Timeout("request timed out".into());
assert_eq!(e.to_string(), "timeout: request timed out");
}
#[test]
fn other_message() {
let e = IbError::Other("something weird".into());
assert_eq!(e.to_string(), "something weird");
}
#[test]
fn notice_10197_is_competing_session() {
let e = classify_notice(10197, "Competing live session", "");
assert!(matches!(e, IbError::CompetingSession));
}
#[test]
fn notice_10000_10999_is_market_data() {
let e = classify_notice(10199, "Requested market data is not subscribed", "");
assert!(matches!(e, IbError::MarketData { code: 10199, .. }));
}
#[test]
fn notice_2107_is_farm_disconnect() {
let e = classify_notice(2107, "HMDS data farm connection is broken", "");
assert!(matches!(e, IbError::FarmDisconnect { .. }));
}
#[test]
fn notice_200_299_is_order_rejected() {
let e =
classify_notice(201, "Order rejected", "{\"reason\":\"insufficient funds\"}");
assert!(matches!(e, IbError::OrderRejected { code: 201, .. }));
if let IbError::OrderRejected { rejection_json, .. } = &e {
assert!(rejection_json.is_some());
}
}
#[test]
fn notice_200_299_order_rejected_no_json() {
let e = classify_notice(202, "Order cancelled", "");
assert!(matches!(e, IbError::OrderRejected { code: 202, .. }));
if let IbError::OrderRejected { rejection_json, .. } = &e {
assert!(rejection_json.is_none());
}
}
#[test]
fn notice_504_is_connection_failed() {
let e = classify_notice(504, "Not connected", "");
assert!(matches!(e, IbError::ConnectionFailed(_)));
}
#[test]
fn notice_unknown_falls_to_other() {
let e = classify_notice(9999, "Unknown code", "");
assert!(matches!(e, IbError::Other(_)));
}
#[test]
fn ibapi_connection_failed_maps_directly() {
let err = ibapi::Error::ConnectionFailed;
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionFailed(_)));
}
#[test]
fn ibapi_connection_reset_maps_directly() {
let err = ibapi::Error::ConnectionReset;
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionReset));
}
#[test]
fn ibapi_notice_10197_maps_to_competing_session() {
let notice = ibapi::Notice {
code: 10197,
message: "Competing live session".into(),
error_time: None,
advanced_order_reject_json: String::new(),
};
let err = ibapi::Error::Notice(notice);
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::CompetingSession));
}
#[test]
fn ibapi_io_connection_reset_maps() {
let io_err = std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection reset by peer",
);
let ibe: IbError = io_err.into();
assert!(matches!(ibe, IbError::ConnectionReset));
}
#[test]
fn ibapi_io_timeout_maps() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
let ibe: IbError = io_err.into();
assert!(matches!(ibe, IbError::Timeout(_)));
}
#[test]
fn ibapi_shutdown_maps_to_connection_reset() {
let err = ibapi::Error::Shutdown;
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionReset));
}
#[test]
fn ibapi_other_maps_to_other() {
let err = ibapi::Error::NotImplemented;
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::Other(_)));
}
#[test]
fn io_broken_pipe_is_connection_reset() {
let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionReset));
}
#[test]
fn io_connection_refused_is_connection_reset() {
let err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionReset));
}
#[test]
fn io_other_is_connection_failed() {
let err =
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
let ibe: IbError = err.into();
assert!(matches!(ibe, IbError::ConnectionFailed(_)));
}
}