Skip to main content

nautilus_okx/common/
failure.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//! OKX mapping from transport errors to [`CommandFailure`].
17
18use nautilus_live::execution::failure::CommandFailure;
19
20use crate::{
21    common::consts::{OKX_ORDER_REQUEST_TIMEOUT_CODE, should_retry_error_code},
22    http::error::OKXHttpError,
23    websocket::error::OKXWsError,
24};
25
26/// Classifies a structured OKX venue code.
27///
28/// Retryable system and rate-limit codes refuse this attempt but do not prove
29/// the command was not applied.
30#[must_use]
31pub fn classify_okx_venue_code(error_code: &str, reason: impl Into<String>) -> CommandFailure {
32    let reason = reason.into();
33
34    if error_code.is_empty()
35        || should_retry_error_code(error_code)
36        || error_code == OKX_ORDER_REQUEST_TIMEOUT_CODE
37    {
38        CommandFailure::Ambiguous(reason)
39    } else {
40        CommandFailure::VenueRejected(reason)
41    }
42}
43
44/// Classifies an OKX HTTP command failure from typed error evidence.
45#[must_use]
46pub fn classify_okx_http_failure(error: &OKXHttpError) -> CommandFailure {
47    let reason = error.to_string();
48    match error {
49        OKXHttpError::MissingCredentials | OKXHttpError::ValidationError(_) => {
50            CommandFailure::NotSent(reason)
51        }
52        OKXHttpError::OkxError { error_code, .. } => classify_okx_venue_code(error_code, reason),
53        OKXHttpError::JsonError(_)
54        | OKXHttpError::Canceled(_)
55        | OKXHttpError::HttpClientError(_)
56        | OKXHttpError::UnexpectedStatus { .. }
57        | OKXHttpError::OperationTimeout { .. }
58        | OKXHttpError::RetryBudgetExceeded(_)
59        | OKXHttpError::EmptyResponse => CommandFailure::Ambiguous(reason),
60    }
61}
62
63/// Classifies an OKX WebSocket command failure from typed error evidence.
64#[must_use]
65pub fn classify_okx_ws_failure(error: &OKXWsError) -> CommandFailure {
66    let reason = error.to_string();
67    match error {
68        OKXWsError::ClientError(_)
69        | OKXWsError::JsonError(_)
70        | OKXWsError::NoActiveClient
71        | OKXWsError::HandlerUnavailable(_) => CommandFailure::NotSent(reason),
72        OKXWsError::OkxError { error_code, .. } => classify_okx_venue_code(error_code, reason),
73        OKXWsError::ParsingError(_)
74        | OKXWsError::AuthenticationError(_)
75        | OKXWsError::TungsteniteError(_)
76        | OKXWsError::SendFailed(_)
77        | OKXWsError::OperationTimeout { .. } => CommandFailure::Ambiguous(reason),
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use nautilus_network::http::{HttpClientError, StatusCode};
84    use rstest::rstest;
85
86    use super::*;
87
88    #[rstest]
89    #[case::parameter_reject("51000", "Parameter state error", true)]
90    #[case::system_busy("50013", "System busy, please retry later", false)]
91    #[case::request_timeout("50004", "API endpoint request timeout", false)]
92    #[case::order_timeout("51149", "Order timed out. Please try again.", false)]
93    #[case::rate_limit("50011", "Request too frequent", false)]
94    #[case::missing_code("", "All operations failed", false)]
95    fn test_classify_okx_venue_code(
96        #[case] error_code: &str,
97        #[case] message: &str,
98        #[case] venue_rejected: bool,
99    ) {
100        let failure = classify_okx_venue_code(error_code, message);
101
102        if venue_rejected {
103            assert_eq!(failure, CommandFailure::VenueRejected(message.to_string()));
104        } else {
105            assert_eq!(failure, CommandFailure::Ambiguous(message.to_string()));
106        }
107    }
108
109    #[rstest]
110    fn test_classify_okx_http_missing_credentials_is_not_sent() {
111        let error = OKXHttpError::MissingCredentials;
112
113        assert_eq!(
114            classify_okx_http_failure(&error),
115            CommandFailure::NotSent(error.to_string())
116        );
117    }
118
119    #[rstest]
120    fn test_classify_okx_http_validation_is_not_sent() {
121        let error = OKXHttpError::ValidationError("invalid quantity".to_string());
122
123        assert_eq!(
124            classify_okx_http_failure(&error),
125            CommandFailure::NotSent(error.to_string())
126        );
127    }
128
129    #[rstest]
130    fn test_classify_okx_http_invalid_retry_config_is_not_sent() {
131        let error = OKXHttpError::ValidationError("invalid retry configuration".to_string());
132
133        assert_eq!(
134            classify_okx_http_failure(&error),
135            CommandFailure::NotSent(error.to_string())
136        );
137    }
138
139    #[rstest]
140    fn test_classify_okx_http_timeout_is_ambiguous() {
141        let error = OKXHttpError::OperationTimeout { timeout_ms: 1_000 };
142
143        assert_eq!(
144            classify_okx_http_failure(&error),
145            CommandFailure::Ambiguous(error.to_string())
146        );
147    }
148
149    #[rstest]
150    fn test_classify_okx_http_budget_is_ambiguous() {
151        let error = OKXHttpError::RetryBudgetExceeded("budget exceeded".to_string());
152
153        assert_eq!(
154            classify_okx_http_failure(&error),
155            CommandFailure::Ambiguous(error.to_string())
156        );
157    }
158
159    #[rstest]
160    fn test_classify_okx_http_network_is_ambiguous() {
161        let error = OKXHttpError::HttpClientError(HttpClientError::Error("timeout".to_string()));
162
163        assert_eq!(
164            classify_okx_http_failure(&error),
165            CommandFailure::Ambiguous(error.to_string())
166        );
167    }
168
169    #[rstest]
170    fn test_classify_okx_http_unexpected_status_is_ambiguous() {
171        let error = OKXHttpError::UnexpectedStatus {
172            status: StatusCode::INTERNAL_SERVER_ERROR,
173            body: String::new(),
174        };
175
176        assert_eq!(
177            classify_okx_http_failure(&error),
178            CommandFailure::Ambiguous(error.to_string())
179        );
180    }
181
182    #[rstest]
183    fn test_classify_okx_http_json_is_ambiguous() {
184        let error = OKXHttpError::JsonError("failed to deserialize".to_string());
185
186        assert_eq!(
187            classify_okx_http_failure(&error),
188            CommandFailure::Ambiguous(error.to_string())
189        );
190    }
191
192    #[rstest]
193    fn test_classify_okx_http_empty_response_is_ambiguous() {
194        let error = OKXHttpError::EmptyResponse;
195
196        assert_eq!(
197            classify_okx_http_failure(&error),
198            CommandFailure::Ambiguous(error.to_string())
199        );
200    }
201
202    #[rstest]
203    fn test_classify_okx_ws_handler_unavailable_is_not_sent() {
204        let error = OKXWsError::HandlerUnavailable("channel closed".to_string());
205
206        assert_eq!(
207            classify_okx_ws_failure(&error),
208            CommandFailure::NotSent(error.to_string())
209        );
210    }
211
212    #[rstest]
213    fn test_classify_okx_ws_no_active_client_is_not_sent() {
214        let error = OKXWsError::NoActiveClient;
215
216        assert_eq!(
217            classify_okx_ws_failure(&error),
218            CommandFailure::NotSent(error.to_string())
219        );
220    }
221
222    #[rstest]
223    fn test_classify_okx_ws_json_encode_is_not_sent() {
224        let error = OKXWsError::JsonError("Failed to serialize order: eof".to_string());
225
226        assert_eq!(
227            classify_okx_ws_failure(&error),
228            CommandFailure::NotSent(error.to_string())
229        );
230    }
231
232    #[rstest]
233    fn test_classify_okx_ws_send_failed_is_ambiguous() {
234        let error = OKXWsError::SendFailed("connection reset".to_string());
235
236        assert_eq!(
237            classify_okx_ws_failure(&error),
238            CommandFailure::Ambiguous(error.to_string())
239        );
240    }
241
242    #[rstest]
243    fn test_classify_okx_ws_timeout_is_ambiguous() {
244        let error = OKXWsError::OperationTimeout { timeout_ms: 2_000 };
245
246        assert_eq!(
247            classify_okx_ws_failure(&error),
248            CommandFailure::Ambiguous(error.to_string())
249        );
250    }
251}