nautilus_okx/common/consts.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//! Core constants shared across the OKX adapter components.
17
18use std::sync::LazyLock;
19
20use ahash::AHashSet;
21use nautilus_model::{
22 enums::{OrderType, TimeInForce},
23 identifiers::{ClientId, Venue},
24};
25use ustr::Ustr;
26
27use super::enums::OKXInstrumentType;
28
29/// Venue identifier string.
30pub const OKX: &str = "OKX";
31
32/// Static venue instance.
33pub static OKX_VENUE: LazyLock<Venue> = LazyLock::new(|| Venue::new(Ustr::from(OKX)));
34
35/// Static client ID instance.
36pub static OKX_CLIENT_ID: LazyLock<ClientId> = LazyLock::new(|| ClientId::new(Ustr::from(OKX)));
37
38/// See <https://www.okx.com/docs-v5/en/#overview-broker-program> for further details.
39pub const OKX_NAUTILUS_BROKER_ID: &str = "5328c82e5542BCDE";
40
41// Use the canonical host with www to avoid cross-domain redirects which may
42// strip authentication headers in some HTTP clients and middleboxes.
43pub const OKX_HTTP_URL: &str = "https://www.okx.com";
44pub const OKX_WS_PUBLIC_URL: &str = "wss://ws.okx.com:8443/ws/v5/public";
45pub const OKX_WS_PRIVATE_URL: &str = "wss://ws.okx.com:8443/ws/v5/private";
46pub const OKX_WS_BUSINESS_URL: &str = "wss://ws.okx.com:8443/ws/v5/business";
47pub const OKX_WS_DEMO_PUBLIC_URL: &str = "wss://wspap.okx.com:8443/ws/v5/public";
48pub const OKX_WS_DEMO_PRIVATE_URL: &str = "wss://wspap.okx.com:8443/ws/v5/private";
49pub const OKX_WS_DEMO_BUSINESS_URL: &str = "wss://wspap.okx.com:8443/ws/v5/business";
50
51pub const OKX_WS_TOPIC_DELIMITER: char = ':';
52
53/// WebSocket heartbeat (ping/pong) interval in seconds.
54pub const OKX_WS_HEARTBEAT_SECS: u64 = 20;
55
56/// OKX success response code for WebSocket operations.
57pub const OKX_SUCCESS_CODE: &str = "0";
58
59/// OKX WebSocket code indicating a service upgrade and required reconnect.
60pub const OKX_SERVICE_UPGRADE_RECONNECT_CODE: &str = "64008";
61
62/// JSON field key for sub-error code in order operation responses.
63pub const OKX_FIELD_SCODE: &str = "sCode";
64
65/// JSON field key for sub-error message in order operation responses.
66pub const OKX_FIELD_SMSG: &str = "sMsg";
67
68/// JSON field key for detailed sub-error code in order operation responses.
69pub const OKX_FIELD_SUBCODE: &str = "subCode";
70
71/// JSON field key for client order ID in order operation responses.
72pub const OKX_FIELD_CLORDID: &str = "clOrdId";
73
74/// Maximum length of a `clOrdId` accepted by OKX.
75///
76/// OKX requires `clOrdId` to be 1-32 case-sensitive alphanumeric characters.
77/// See <https://www.okx.com/docs-v5/en/#order-book-trading-trade>.
78pub const OKX_MAX_CLORDID_LEN: usize = 32;
79
80/// Validates a `clOrdId` against OKX's length and charset rules.
81///
82/// # Errors
83///
84/// Returns a human-readable reason when the ID exceeds
85/// [`OKX_MAX_CLORDID_LEN`] characters or contains non-alphanumeric characters
86/// (such as hyphens or underscores).
87pub fn validate_okx_client_order_id(cl_ord_id: &str) -> Result<(), String> {
88 let len = cl_ord_id.len();
89 if len > OKX_MAX_CLORDID_LEN {
90 return Err(format!(
91 "OKX requires clOrdId to be at most {OKX_MAX_CLORDID_LEN} characters, was {len} ({cl_ord_id:?}); \
92 set `use_uuid_client_order_ids=True` and `use_hyphens_in_client_order_ids=False` on the strategy config"
93 ));
94 }
95
96 if !cl_ord_id.bytes().all(|b| b.is_ascii_alphanumeric()) {
97 return Err(format!(
98 "OKX requires clOrdId to be alphanumeric only, was {cl_ord_id:?}; \
99 set `use_hyphens_in_client_order_ids=False` on the strategy config"
100 ));
101 }
102
103 Ok(())
104}
105
106/// OKX supported order time in force.
107///
108/// # Notes
109///
110/// - OKX implements IOC and FOK as order types rather than separate time-in-force parameters.
111/// - FOK is only supported with Limit orders (Market + FOK is not supported).
112/// - IOC with Market orders uses OptimalLimitIoc, with Limit orders uses Ioc.
113/// - GTD is supported via expire_time parameter.
114pub const OKX_SUPPORTED_TIME_IN_FORCE: &[TimeInForce] = &[
115 TimeInForce::Gtc, // Good Till Cancel (default)
116 TimeInForce::Ioc, // Immediate or Cancel (mapped to OKXOrderType::Ioc or OptimalLimitIoc)
117 TimeInForce::Fok, // Fill or Kill (only with Limit orders, mapped to OKXOrderType::Fok)
118];
119
120/// OKX supported order types.
121///
122/// # Notes
123///
124/// - PostOnly is supported as a flag on limit orders.
125/// - Conditional orders (stop/trigger) are supported via algo orders.
126pub const OKX_SUPPORTED_ORDER_TYPES: &[OrderType] = &[
127 OrderType::Market,
128 OrderType::Limit,
129 OrderType::MarketToLimit, // Mapped to IOC when no price is specified
130 OrderType::StopMarket, // Supported via algo order API
131 OrderType::StopLimit, // Supported via algo order API
132 OrderType::MarketIfTouched, // Supported via algo order API
133 OrderType::LimitIfTouched, // Supported via algo order API
134 OrderType::TrailingStopMarket, // Supported via algo order API (move_order_stop)
135];
136
137/// Conditional order types that require the OKX algo order API.
138pub const OKX_CONDITIONAL_ORDER_TYPES: &[OrderType] = &[
139 OrderType::StopMarket,
140 OrderType::StopLimit,
141 OrderType::MarketIfTouched,
142 OrderType::LimitIfTouched,
143 OrderType::TrailingStopMarket,
144];
145
146/// Advance algo order types that require `cancel-advance-algos` for cancellation.
147/// These cannot be cancelled via the standard `cancel-algos` endpoint.
148pub const OKX_ADVANCE_ALGO_ORDER_TYPES: &[OrderType] = &[OrderType::TrailingStopMarket];
149
150/// OKX error codes that should trigger retries.
151///
152/// Only retry on temporary network/system issues. `50004` ("request
153/// timeout, outcome unknown") is safe because every order/cancel/amend
154/// path sends `clOrdId` and OKX rejects duplicates with `51000`.
155///
156/// # References
157///
158/// Based on OKX API documentation: <https://www.okx.com/docs-v5/en/#error-codes>
159pub static OKX_RETRY_ERROR_CODES: LazyLock<AHashSet<&'static str>> = LazyLock::new(|| {
160 let mut codes = AHashSet::new();
161
162 // Temporary system errors
163 codes.insert("50001"); // Service temporarily unavailable
164 codes.insert("50004"); // API endpoint request timeout (does not mean that the request was successful or failed, please check the request result)
165 codes.insert("50005"); // API is offline or unavailable
166 codes.insert("50013"); // System busy, please try again later
167 codes.insert("50026"); // System error, please try again later
168
169 // Rate limit errors (temporary)
170 codes.insert("50011"); // Request too frequent
171 codes.insert("50113"); // API requests exceed the limit
172
173 // WebSocket connection issues (temporary)
174 codes.insert("60001"); // OK not received in time
175 codes.insert("60005"); // Connection closed as there was no data transmission in the last 30 seconds
176 codes.insert(OKX_SERVICE_UPGRADE_RECONNECT_CODE); // Service upgrade, please reconnect
177
178 codes
179});
180
181/// Determines if an OKX error code should trigger a retry.
182pub fn should_retry_error_code(error_code: &str) -> bool {
183 OKX_RETRY_ERROR_CODES.contains(error_code)
184}
185
186/// OKX error code returned when a post-only order would immediately take liquidity.
187pub const OKX_POST_ONLY_ERROR_CODE: &str = "51019";
188
189/// OKX cancel source code used when a post-only order is auto-cancelled for taking liquidity.
190pub const OKX_POST_ONLY_CANCEL_SOURCE: &str = "31";
191
192/// Human-readable reason used when a post-only order is auto-cancelled for taking liquidity.
193pub const OKX_POST_ONLY_CANCEL_REASON: &str = "POST_ONLY would take liquidity";
194
195/// OKX error code returned when a market order's `slippagePct` would be exceeded by the
196/// projected fill, so the order is rejected.
197pub const OKX_SLIPPAGE_EXCEEDED_ERROR_CODE: &str = "54084";
198
199/// OKX error code returned when the supplied `slippagePct` value is outside the
200/// venue-permitted range.
201pub const OKX_SLIPPAGE_INVALID_ERROR_CODE: &str = "54085";
202
203/// Returns `true` if the OKX `sCode` identifies a slippage-related rejection emitted
204/// in response to the `slippagePct` parameter on market orders.
205#[must_use]
206pub fn is_slippage_rejection(error_code: &str) -> bool {
207 matches!(
208 error_code,
209 OKX_SLIPPAGE_EXCEEDED_ERROR_CODE | OKX_SLIPPAGE_INVALID_ERROR_CODE
210 )
211}
212
213/// Target currency literal for base currency.
214pub const OKX_TARGET_CCY_BASE: &str = "base_ccy";
215
216/// Target currency literal for quote currency.
217pub const OKX_TARGET_CCY_QUOTE: &str = "quote_ccy";
218
219/// Resolves instrument families for a given instrument type.
220///
221/// Returns `Some(families)` when the type supports family filtering, or `None`
222/// to skip the instrument type entirely (Option without configured families).
223/// An empty vec means no family filter is needed (Spot, Margin), or all
224/// discoverable families should be loaded (Events).
225pub fn resolve_instrument_families(
226 configured: &Option<Vec<String>>,
227 inst_type: OKXInstrumentType,
228) -> Option<Vec<String>> {
229 match (configured, inst_type) {
230 (Some(families), OKXInstrumentType::Option) => Some(families.clone()),
231 (
232 Some(families),
233 OKXInstrumentType::Futures | OKXInstrumentType::Swap | OKXInstrumentType::Events,
234 ) => Some(families.clone()),
235 (None, OKXInstrumentType::Option) => {
236 log::warn!("Skipping OPTION type: instrument_families required but not configured");
237 None
238 }
239 _ => Some(vec![]),
240 }
241}
242
243/// Clamps a requested book depth to the nearest OKX-supported value.
244///
245/// OKX WebSocket channels support depths of 50 and 400. Depth 0 means
246/// auto-select based on VIP level. Any other value rounds up to the nearest
247/// supported depth so the subscription succeeds and the data engine can
248/// truncate to the originally requested depth.
249pub fn resolve_book_depth(raw_depth: usize) -> usize {
250 match raw_depth {
251 0 | 400 => raw_depth,
252 1..=50 => 50,
253 _ => 400,
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use rstest::rstest;
260
261 use super::*;
262
263 #[rstest]
264 #[case("54084", true)]
265 #[case("54085", true)]
266 #[case("51019", false)]
267 #[case("", false)]
268 fn test_is_slippage_rejection(#[case] code: &str, #[case] expected: bool) {
269 assert_eq!(is_slippage_rejection(code), expected);
270 }
271
272 #[rstest]
273 #[case("50001", true)]
274 #[case("60005", true)]
275 #[case(OKX_SERVICE_UPGRADE_RECONNECT_CODE, true)]
276 #[case("60012", false)]
277 fn test_should_retry_error_code(#[case] code: &str, #[case] expected: bool) {
278 assert_eq!(should_retry_error_code(code), expected);
279 }
280
281 #[rstest]
282 #[case("O20260101000000ABC1", true)]
283 #[case("aB9", true)]
284 #[case("abcdefghij0123456789ABCDEFGHIJ12", true)] // exactly 32 chars
285 #[case("abcdefghij0123456789ABCDEFGHIJ123", false)] // 33 chars
286 #[case("O-20260101-000000-001-001-1", false)] // hyphens
287 #[case("O_20260101_000000", false)] // underscores
288 #[case("", true)] // empty: OKX rejects, but core ClientOrderId never produces empty
289 fn test_validate_okx_client_order_id(#[case] cl_ord_id: &str, #[case] expected_ok: bool) {
290 assert_eq!(validate_okx_client_order_id(cl_ord_id).is_ok(), expected_ok);
291 }
292
293 #[rstest]
294 fn test_validate_okx_client_order_id_length_message() {
295 // 35-char compact ID (the shape reported in the original bug report).
296 let cl_ord_id = "O20260522145501532392555aceLTCUSDT5";
297 let err = validate_okx_client_order_id(cl_ord_id).unwrap_err();
298 assert!(err.contains("at most 32"));
299 assert!(err.contains("was 35"));
300 assert!(err.contains("use_uuid_client_order_ids"));
301 }
302}