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;
19use nautilus_network::{error::SendError, http::HttpClientError};
20
21use crate::{
22    common::consts::{OKX_ORDER_REQUEST_TIMEOUT_CODE, should_retry_error_code},
23    http::error::OKXHttpError,
24    websocket::error::OKXWsError,
25};
26
27/// Classifies a structured OKX venue code.
28///
29/// Retryable system and rate-limit codes refuse this attempt but do not prove
30/// the command was not applied.
31#[must_use]
32pub fn classify_okx_venue_code(error_code: &str, reason: impl Into<String>) -> CommandFailure {
33    let reason = reason.into();
34
35    if error_code.is_empty()
36        || should_retry_error_code(error_code)
37        || error_code == OKX_ORDER_REQUEST_TIMEOUT_CODE
38    {
39        CommandFailure::Ambiguous(reason)
40    } else {
41        CommandFailure::VenueRejected(reason)
42    }
43}
44
45/// Classifies an OKX HTTP command failure from typed error evidence.
46#[must_use]
47pub fn classify_okx_http_failure(error: &OKXHttpError) -> CommandFailure {
48    let reason = error.to_string();
49    match error {
50        OKXHttpError::MissingCredentials
51        | OKXHttpError::ValidationError(_)
52        | OKXHttpError::RequestSerialization(_)
53        | OKXHttpError::HttpClientError(
54            HttpClientError::InvalidProxy(_) | HttpClientError::ClientBuildError(_),
55        ) => CommandFailure::NotSent(reason),
56        OKXHttpError::OkxError { error_code, .. }
57        | OKXHttpError::RetryableOkxError { error_code, .. } => {
58            classify_okx_venue_code(error_code, reason)
59        }
60        OKXHttpError::MalformedResponse(_)
61        | OKXHttpError::ResponseDecoding(_)
62        | OKXHttpError::Canceled(_)
63        | OKXHttpError::HttpClientError(_)
64        | OKXHttpError::RetryableStatus { .. }
65        | OKXHttpError::UnexpectedStatus { .. }
66        | OKXHttpError::OperationTimeout { .. }
67        | OKXHttpError::RetryBudgetExceeded(_)
68        | OKXHttpError::EmptyResponse => CommandFailure::Ambiguous(reason),
69    }
70}
71
72/// Classifies an OKX WebSocket command failure from typed error evidence.
73#[must_use]
74pub fn classify_okx_ws_failure(error: &OKXWsError) -> CommandFailure {
75    let reason = error.to_string();
76    match error {
77        OKXWsError::ClientError(_)
78        | OKXWsError::JsonError(_)
79        | OKXWsError::NoActiveClient
80        | OKXWsError::HandlerUnavailable(_)
81        | OKXWsError::TransportSend(
82            SendError::InvalidInput(_)
83            | SendError::Closed
84            | SendError::Timeout
85            | SendError::ConnectionChanged,
86        ) => CommandFailure::NotSent(reason),
87        OKXWsError::OkxError { error_code, .. } => classify_okx_venue_code(error_code, reason),
88        OKXWsError::ParsingError(_)
89        | OKXWsError::AuthenticationError(_)
90        | OKXWsError::TungsteniteError(_)
91        | OKXWsError::TransportSend(SendError::WriteTimeout | SendError::BrokenPipe(_))
92        | OKXWsError::SendFailed(_)
93        | OKXWsError::OperationTimeout { .. } => CommandFailure::Ambiguous(reason),
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use nautilus_network::http::{HttpClientError, StatusCode};
100    use rstest::rstest;
101
102    use super::*;
103
104    #[rstest]
105    #[case::parameter_reject("51000", "Parameter state error", true)]
106    #[case::system_busy("50013", "System busy, please retry later", false)]
107    #[case::request_timeout("50004", "API endpoint request timeout", false)]
108    #[case::order_timeout("51149", "Order timed out. Please try again.", false)]
109    #[case::rate_limit("50011", "Request too frequent", false)]
110    #[case::ws_rate_limit("60014", "WebSocket requests too frequent", true)]
111    #[case::ws_internal_error("64007", "WebSocket internal error", true)]
112    #[case::invalid_signature("50113", "Invalid signature", true)]
113    #[case::missing_code("", "All operations failed", false)]
114    fn test_classify_okx_venue_code(
115        #[case] error_code: &str,
116        #[case] message: &str,
117        #[case] venue_rejected: bool,
118    ) {
119        let failure = classify_okx_venue_code(error_code, message);
120
121        if venue_rejected {
122            assert_eq!(failure, CommandFailure::VenueRejected(message.to_string()));
123        } else {
124            assert_eq!(failure, CommandFailure::Ambiguous(message.to_string()));
125        }
126    }
127
128    #[rstest]
129    fn test_classify_okx_http_permanent_venue_error_is_rejected() {
130        let error = OKXHttpError::OkxError {
131            error_code: "51000".to_string(),
132            message: "Parameter state error".to_string(),
133        };
134        let reason = error.to_string();
135
136        assert_eq!(
137            classify_okx_http_failure(&error),
138            CommandFailure::VenueRejected(reason)
139        );
140    }
141
142    #[rstest]
143    fn test_classify_okx_http_retryable_venue_error_is_ambiguous() {
144        let error = OKXHttpError::RetryableOkxError {
145            error_code: "50013".to_string(),
146            message: "System busy, please try again later".to_string(),
147            retry_after: None,
148        };
149        let reason = error.to_string();
150
151        assert_eq!(
152            classify_okx_http_failure(&error),
153            CommandFailure::Ambiguous(reason)
154        );
155    }
156
157    #[rstest]
158    fn test_classify_okx_http_missing_credentials_is_not_sent() {
159        let error = OKXHttpError::MissingCredentials;
160
161        assert_eq!(
162            classify_okx_http_failure(&error),
163            CommandFailure::NotSent(error.to_string())
164        );
165    }
166
167    #[rstest]
168    fn test_classify_okx_http_validation_is_not_sent() {
169        let error = OKXHttpError::ValidationError("invalid quantity".to_string());
170
171        assert_eq!(
172            classify_okx_http_failure(&error),
173            CommandFailure::NotSent(error.to_string())
174        );
175    }
176
177    #[rstest]
178    fn test_classify_okx_http_invalid_retry_config_is_not_sent() {
179        let error = OKXHttpError::ValidationError("invalid retry configuration".to_string());
180
181        assert_eq!(
182            classify_okx_http_failure(&error),
183            CommandFailure::NotSent(error.to_string())
184        );
185    }
186
187    #[rstest]
188    fn test_classify_okx_http_timeout_is_ambiguous() {
189        let error = OKXHttpError::OperationTimeout { timeout_ms: 1_000 };
190
191        assert_eq!(
192            classify_okx_http_failure(&error),
193            CommandFailure::Ambiguous(error.to_string())
194        );
195    }
196
197    #[rstest]
198    fn test_classify_okx_http_budget_is_ambiguous() {
199        let error = OKXHttpError::RetryBudgetExceeded("budget exceeded".to_string());
200
201        assert_eq!(
202            classify_okx_http_failure(&error),
203            CommandFailure::Ambiguous(error.to_string())
204        );
205    }
206
207    #[rstest]
208    fn test_classify_okx_http_network_is_ambiguous() {
209        let error = OKXHttpError::HttpClientError(HttpClientError::TransportError(
210            "connection reset".to_string(),
211        ));
212
213        assert_eq!(
214            classify_okx_http_failure(&error),
215            CommandFailure::Ambiguous(error.to_string())
216        );
217    }
218
219    #[rstest]
220    fn test_classify_okx_http_permanent_client_error_is_ambiguous() {
221        let error = OKXHttpError::HttpClientError(HttpClientError::Error(
222            "response body exceeds maximum".to_string(),
223        ));
224
225        assert_eq!(
226            classify_okx_http_failure(&error),
227            CommandFailure::Ambiguous(error.to_string())
228        );
229    }
230
231    #[rstest]
232    fn test_classify_okx_http_unexpected_status_is_ambiguous() {
233        let error = OKXHttpError::UnexpectedStatus {
234            status: StatusCode::INTERNAL_SERVER_ERROR,
235            body: String::new(),
236        };
237
238        assert_eq!(
239            classify_okx_http_failure(&error),
240            CommandFailure::Ambiguous(error.to_string())
241        );
242    }
243
244    #[rstest]
245    fn test_classify_okx_http_response_decoding_is_ambiguous() {
246        let error = OKXHttpError::ResponseDecoding("failed to deserialize".to_string());
247
248        assert_eq!(
249            classify_okx_http_failure(&error),
250            CommandFailure::Ambiguous(error.to_string())
251        );
252    }
253
254    #[rstest]
255    fn test_classify_okx_http_request_serialization_is_not_sent() {
256        let error = OKXHttpError::RequestSerialization("failed to serialize".to_string());
257
258        assert_eq!(
259            classify_okx_http_failure(&error),
260            CommandFailure::NotSent(error.to_string())
261        );
262    }
263
264    #[rstest]
265    fn test_classify_okx_http_empty_response_is_ambiguous() {
266        let error = OKXHttpError::EmptyResponse;
267
268        assert_eq!(
269            classify_okx_http_failure(&error),
270            CommandFailure::Ambiguous(error.to_string())
271        );
272    }
273
274    #[rstest]
275    fn test_classify_okx_ws_handler_unavailable_is_not_sent() {
276        let error = OKXWsError::HandlerUnavailable("channel closed".to_string());
277
278        assert_eq!(
279            classify_okx_ws_failure(&error),
280            CommandFailure::NotSent(error.to_string())
281        );
282    }
283
284    #[rstest]
285    fn test_classify_okx_ws_no_active_client_is_not_sent() {
286        let error = OKXWsError::NoActiveClient;
287
288        assert_eq!(
289            classify_okx_ws_failure(&error),
290            CommandFailure::NotSent(error.to_string())
291        );
292    }
293
294    #[rstest]
295    fn test_classify_okx_ws_json_encode_is_not_sent() {
296        let error = OKXWsError::JsonError("Failed to serialize order: eof".to_string());
297
298        assert_eq!(
299            classify_okx_ws_failure(&error),
300            CommandFailure::NotSent(error.to_string())
301        );
302    }
303
304    #[rstest]
305    fn test_classify_okx_ws_send_failed_is_ambiguous() {
306        let error = OKXWsError::SendFailed("connection reset".to_string());
307
308        assert_eq!(
309            classify_okx_ws_failure(&error),
310            CommandFailure::Ambiguous(error.to_string())
311        );
312    }
313
314    #[rstest]
315    fn test_classify_okx_ws_pre_write_timeout_is_not_sent() {
316        let error = OKXWsError::TransportSend(SendError::Timeout);
317
318        assert_eq!(
319            classify_okx_ws_failure(&error),
320            CommandFailure::NotSent(error.to_string())
321        );
322    }
323
324    #[rstest]
325    fn test_classify_okx_ws_write_timeout_is_ambiguous() {
326        let error = OKXWsError::TransportSend(SendError::WriteTimeout);
327
328        assert_eq!(
329            classify_okx_ws_failure(&error),
330            CommandFailure::Ambiguous(error.to_string())
331        );
332    }
333
334    #[rstest]
335    fn test_classify_okx_ws_timeout_is_ambiguous() {
336        let error = OKXWsError::OperationTimeout { timeout_ms: 2_000 };
337
338        assert_eq!(
339            classify_okx_ws_failure(&error),
340            CommandFailure::Ambiguous(error.to_string())
341        );
342    }
343}