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
64/// Result type for Interactive Brokers operations.
65pub type InteractiveBrokersResult<T> = Result<T, InteractiveBrokersError>;
66
67/// IB API error code classification.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum ErrorCategory {
70    /// Client/application error (should not retry).
71    ClientError,
72    /// Connectivity error (should retry with backoff).
73    ConnectivityError,
74    /// Subscription error (may need resubscription).
75    SubscriptionError,
76    /// Order error (may need special handling).
77    OrderError,
78    /// Market data error (may need resubscription).
79    MarketDataError,
80    /// Unknown/unclassified error.
81    Unknown,
82}
83
84/// Classify an IB error code into a category.
85///
86/// # Arguments
87///
88/// * `error_code` - The IB API error code
89///
90/// # Returns
91///
92/// Returns the error category for the given code.
93pub fn classify_error_code(error_code: i32) -> ErrorCategory {
94    match error_code {
95        // Client errors - should not retry
96        200..=299 => ErrorCategory::ClientError,
97
98        // Connectivity errors - should retry
99        326 | 502 | 503 | 504 | 1100 | 1101 | 1102 | 1300 | 1301 | 1302 => {
100            ErrorCategory::ConnectivityError
101        }
102
103        // Subscription errors - may need resubscription
104        10189 | 366 | 102 | 10182 => ErrorCategory::SubscriptionError,
105
106        // Market data errors
107        100..=199 if error_code != 10182 => ErrorCategory::MarketDataError,
108
109        // Note: Order errors overlap with client errors range
110        // We handle order errors separately in the match
111
112        // Unknown
113        _ => ErrorCategory::Unknown,
114    }
115}
116
117/// Determine if an error is recoverable.
118///
119/// # Arguments
120///
121/// * `error_code` - The IB API error code
122///
123/// # Returns
124///
125/// Returns `true` if the error is recoverable (should retry).
126pub fn is_recoverable_error(error_code: i32) -> bool {
127    matches!(
128        classify_error_code(error_code),
129        ErrorCategory::ConnectivityError | ErrorCategory::SubscriptionError
130    )
131}
132
133/// Determine if an error requires subscription resubscription.
134///
135/// # Arguments
136///
137/// * `error_code` - The IB API error code
138///
139/// # Returns
140///
141/// Returns `true` if subscriptions should be resubscribed.
142pub fn requires_resubscription(error_code: i32) -> bool {
143    matches!(error_code, 10189 | 366 | 102 | 10182)
144}
145
146/// Get a human-readable error description.
147///
148/// # Arguments
149///
150/// * `error_code` - The IB API error code
151/// * `error_string` - The error message from IB
152///
153/// # Returns
154///
155/// Returns a formatted error description.
156pub fn format_error_message(error_code: i32, error_string: &str) -> String {
157    let category = classify_error_code(error_code);
158    let category_str = match category {
159        ErrorCategory::ClientError => "Client Error",
160        ErrorCategory::ConnectivityError => "Connectivity Error",
161        ErrorCategory::SubscriptionError => "Subscription Error",
162        ErrorCategory::OrderError => "Order Error",
163        ErrorCategory::MarketDataError => "Market Data Error",
164        ErrorCategory::Unknown => "Unknown Error",
165    };
166
167    format!(
168        "[{}] {} (Code: {}): {}",
169        category_str, error_string, error_code, error_string
170    )
171}
172
173#[cfg(test)]
174mod tests {
175    use rstest::rstest;
176
177    use super::{ErrorCategory, classify_error_code, is_recoverable_error};
178
179    #[rstest]
180    #[case(326, ErrorCategory::ConnectivityError)]
181    #[case(502, ErrorCategory::ConnectivityError)]
182    #[case(10182, ErrorCategory::SubscriptionError)]
183    #[case(200, ErrorCategory::ClientError)]
184    fn test_classify_error_code(#[case] error_code: i32, #[case] expected: ErrorCategory) {
185        assert_eq!(classify_error_code(error_code), expected);
186    }
187
188    #[rstest]
189    #[case(326, true)]
190    #[case(10182, true)]
191    #[case(200, false)]
192    fn test_is_recoverable_error(#[case] error_code: i32, #[case] expected: bool) {
193        assert_eq!(is_recoverable_error(error_code), expected);
194    }
195}