Skip to main content

nautilus_interactive_brokers/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Error types and classification for the Interactive Brokers adapter.
17
18use thiserror::Error;
19
20/// Errors that can occur in the Interactive Brokers adapter.
21#[derive(Error, Debug)]
22pub enum InteractiveBrokersError {
23    /// Connection error.
24    #[error("Connection error: {0}")]
25    Connection(String),
26
27    /// Authentication error.
28    #[error("Authentication error: {0}")]
29    Authentication(String),
30
31    /// Invalid configuration.
32    #[error("Invalid configuration: {0}")]
33    Configuration(String),
34
35    /// API request error.
36    #[error("API request error: {0}")]
37    Request(String),
38
39    /// Response parsing error.
40    #[error("Response parsing error: {0}")]
41    Parse(String),
42
43    /// Instrument error.
44    #[error("Instrument error: {0}")]
45    Instrument(String),
46
47    /// Order error.
48    #[error("Order error: {0}")]
49    Order(String),
50
51    /// Market data error.
52    #[error("Market data error: {0}")]
53    MarketData(String),
54
55    /// Generic error from rust-ibapi.
56    #[error("IB API error: {0}")]
57    IbApi(String),
58
59    /// Internal error.
60    #[error("Internal error: {0}")]
61    Internal(String),
62}
63
64impl InteractiveBrokersError {
65    /// Returns the payload-free error kind.
66    #[must_use]
67    pub const fn kind(&self) -> InteractiveBrokersErrorKind {
68        match self {
69            Self::Connection(_) => InteractiveBrokersErrorKind::Connection,
70            Self::Authentication(_) => InteractiveBrokersErrorKind::Authentication,
71            Self::Configuration(_) => InteractiveBrokersErrorKind::Configuration,
72            Self::Request(_) => InteractiveBrokersErrorKind::Request,
73            Self::Parse(_) => InteractiveBrokersErrorKind::Parse,
74            Self::Instrument(_) => InteractiveBrokersErrorKind::Instrument,
75            Self::Order(_) => InteractiveBrokersErrorKind::Order,
76            Self::MarketData(_) => InteractiveBrokersErrorKind::MarketData,
77            Self::IbApi(_) => InteractiveBrokersErrorKind::IbApi,
78            Self::Internal(_) => InteractiveBrokersErrorKind::Internal,
79        }
80    }
81}
82
83/// Payload-free Interactive Brokers adapter error kind.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85#[cfg_attr(
86    feature = "python",
87    pyo3::pyclass(
88        module = "nautilus_trader.core.nautilus_pyo3.interactive_brokers",
89        from_py_object
90    )
91)]
92pub enum InteractiveBrokersErrorKind {
93    /// Connection error.
94    Connection,
95    /// Authentication error.
96    Authentication,
97    /// Invalid configuration.
98    Configuration,
99    /// API request error.
100    Request,
101    /// Response parsing error.
102    Parse,
103    /// Instrument error.
104    Instrument,
105    /// Order error.
106    Order,
107    /// Market data error.
108    MarketData,
109    /// Generic error from rust-ibapi.
110    IbApi,
111    /// Internal error.
112    Internal,
113}
114
115/// Result type for Interactive Brokers operations.
116pub type InteractiveBrokersResult<T> = Result<T, InteractiveBrokersError>;
117
118/// IB API error code classification.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[cfg_attr(
121    feature = "python",
122    pyo3::pyclass(
123        module = "nautilus_trader.core.nautilus_pyo3.interactive_brokers",
124        from_py_object
125    )
126)]
127pub enum ErrorCategory {
128    /// Client/application error (should not retry).
129    ClientError,
130    /// Connectivity error (should retry with backoff).
131    ConnectivityError,
132    /// Subscription error (may need resubscription).
133    SubscriptionError,
134    /// Order error (may need special handling).
135    OrderError,
136    /// Market data error (may need resubscription).
137    MarketDataError,
138    /// Unknown/unclassified error.
139    Unknown,
140}
141
142/// Classify an IB error code into a category.
143pub fn classify_error_code(error_code: i32) -> ErrorCategory {
144    match error_code {
145        // Client errors - should not retry
146        200..=299 => ErrorCategory::ClientError,
147
148        // Connectivity errors - should retry
149        326 | 502 | 503 | 504 | 1100 | 1101 | 1102 | 1300 | 1301 | 1302 => {
150            ErrorCategory::ConnectivityError
151        }
152
153        // Subscription errors - may need resubscription
154        10189 | 366 | 102 | 10182 => ErrorCategory::SubscriptionError,
155
156        // Market data errors
157        100..=199 if error_code != 10182 => ErrorCategory::MarketDataError,
158
159        // Note: Order errors overlap with client errors range
160        // We handle order errors separately in the match
161
162        // Unknown
163        _ => ErrorCategory::Unknown,
164    }
165}
166
167/// Determine if an error is recoverable.
168pub fn is_recoverable_error(error_code: i32) -> bool {
169    matches!(
170        classify_error_code(error_code),
171        ErrorCategory::ConnectivityError | ErrorCategory::SubscriptionError
172    )
173}
174
175/// Determine if an error requires subscription resubscription.
176pub fn requires_resubscription(error_code: i32) -> bool {
177    matches!(error_code, 10189 | 366 | 102 | 10182)
178}
179
180/// Get a human-readable error description.
181pub fn format_error_message(error_code: i32, error_string: &str) -> String {
182    let category = classify_error_code(error_code);
183    let category_str = match category {
184        ErrorCategory::ClientError => "Client Error",
185        ErrorCategory::ConnectivityError => "Connectivity Error",
186        ErrorCategory::SubscriptionError => "Subscription Error",
187        ErrorCategory::OrderError => "Order Error",
188        ErrorCategory::MarketDataError => "Market Data Error",
189        ErrorCategory::Unknown => "Unknown Error",
190    };
191
192    format!(
193        "[{}] {} (Code: {}): {}",
194        category_str, error_string, error_code, error_string
195    )
196}
197
198#[cfg(test)]
199mod tests {
200    use rstest::rstest;
201
202    use super::{ErrorCategory, classify_error_code, is_recoverable_error};
203
204    #[rstest]
205    #[case(326, ErrorCategory::ConnectivityError)]
206    #[case(502, ErrorCategory::ConnectivityError)]
207    #[case(10182, ErrorCategory::SubscriptionError)]
208    #[case(200, ErrorCategory::ClientError)]
209    fn test_classify_error_code(#[case] error_code: i32, #[case] expected: ErrorCategory) {
210        assert_eq!(classify_error_code(error_code), expected);
211    }
212
213    #[rstest]
214    #[case(326, true)]
215    #[case(10182, true)]
216    #[case(200, false)]
217    fn test_is_recoverable_error(#[case] error_code: i32, #[case] expected: bool) {
218        assert_eq!(is_recoverable_error(error_code), expected);
219    }
220}