ccxt_exchanges/okx/endpoint_router.rs
1//! OKX-specific endpoint router trait.
2//!
3//! This module provides the `OkxEndpointRouter` trait for routing API requests
4//! to the correct OKX endpoints based on channel type.
5//!
6//! # OKX API Structure
7//!
8//! OKX uses a unified V5 API with the following endpoint structure:
9//! - **REST**: `www.okx.com` - Single unified REST endpoint for all market types
10//! - **WebSocket Public**: `ws.okx.com:8443/ws/v5/public` - Public market data
11//! - **WebSocket Private**: `ws.okx.com:8443/ws/v5/private` - Account data
12//! - **WebSocket Business**: `ws.okx.com:8443/ws/v5/business` - Trade execution
13//!
14//! # Demo Trading Mode
15//!
16//! OKX uses a unique approach for demo trading:
17//! - REST API uses the **same production domain** (`www.okx.com`)
18//! - Demo mode is indicated by the `x-simulated-trading: 1` header
19//! - WebSocket URLs switch to demo domain (`wspap.okx.com:8443`)
20//!
21//! # Channel Types
22//!
23//! OKX WebSocket has three channel types:
24//! - `Public` - Market data (tickers, orderbooks, trades)
25//! - `Private` - Account data (positions, orders, balances)
26//! - `Business` - Trade execution and advanced features
27//!
28//! # Example
29//!
30//! ```rust,no_run
31//! use ccxt_exchanges::okx::{Okx, OkxEndpointRouter, OkxChannelType};
32//! use ccxt_core::ExchangeConfig;
33//!
34//! let okx = Okx::new(ExchangeConfig::default()).unwrap();
35//!
36//! // Get REST endpoint (unified for all market types)
37//! let rest_url = okx.rest_endpoint();
38//! assert!(rest_url.contains("okx.com"));
39//!
40//! // Get WebSocket endpoint for public channel
41//! let ws_public = okx.ws_endpoint(OkxChannelType::Public);
42//! assert!(ws_public.contains("/ws/v5/public"));
43//!
44//! // Get WebSocket endpoint for private channel
45//! let ws_private = okx.ws_endpoint(OkxChannelType::Private);
46//! assert!(ws_private.contains("/ws/v5/private"));
47//!
48//! // Check if demo trading mode is enabled
49//! let is_demo = okx.is_demo_trading();
50//! ```
51
52/// OKX WebSocket channel type.
53///
54/// OKX uses different WebSocket channels for different types of data:
55/// - `Public` - Market data streams (no authentication required)
56/// - `Private` - Account data streams (authentication required)
57/// - `Business` - Trade execution streams (authentication required)
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum OkxChannelType {
60 /// Public channel for market data (tickers, orderbooks, trades).
61 ///
62 /// No authentication required. Used for:
63 /// - Real-time ticker updates
64 /// - Order book snapshots and updates
65 /// - Public trade streams
66 /// - Candlestick/OHLCV data
67 Public,
68
69 /// Private channel for account data.
70 ///
71 /// Authentication required. Used for:
72 /// - Account balance updates
73 /// - Position updates
74 /// - Order status updates
75 Private,
76
77 /// Business channel for trade execution.
78 ///
79 /// Authentication required. Used for:
80 /// - Advanced order types
81 /// - Algo orders
82 /// - Grid trading
83 /// - Copy trading
84 Business,
85}
86
87impl std::fmt::Display for OkxChannelType {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 OkxChannelType::Public => write!(f, "public"),
91 OkxChannelType::Private => write!(f, "private"),
92 OkxChannelType::Business => write!(f, "business"),
93 }
94 }
95}
96
97/// OKX-specific endpoint router trait.
98///
99/// This trait defines methods for obtaining the correct API endpoints for OKX.
100/// OKX uses a unified REST endpoint for all market types, with WebSocket
101/// channels differentiated by channel type (public, private, business).
102///
103/// # Implementation Notes
104///
105/// - REST API uses a single unified domain for all market types
106/// - Demo trading mode uses the same REST domain but adds a special header
107/// - WebSocket endpoints are differentiated by channel type
108/// - Demo mode WebSocket URLs use a different domain (`wspap.okx.com`)
109///
110/// # Demo Trading
111///
112/// OKX's demo trading mode is unique:
113/// - REST requests use the production domain with `x-simulated-trading: 1` header
114/// - WebSocket connections use demo-specific URLs with `brokerId=9999` parameter
115pub trait OkxEndpointRouter {
116 /// Returns the REST API endpoint.
117 ///
118 /// OKX uses a unified REST domain for all market types. The instrument
119 /// type is specified as a query parameter in API requests, not in the URL.
120 ///
121 /// Note: For demo trading, the same URL is used but with the
122 /// `x-simulated-trading: 1` header added to requests.
123 ///
124 /// # Returns
125 ///
126 /// The REST API base URL string.
127 ///
128 /// # Example
129 ///
130 /// ```rust,no_run
131 /// use ccxt_exchanges::okx::{Okx, OkxEndpointRouter};
132 /// use ccxt_core::ExchangeConfig;
133 ///
134 /// let okx = Okx::new(ExchangeConfig::default()).unwrap();
135 /// let url = okx.rest_endpoint();
136 /// assert_eq!(url, "https://www.okx.com");
137 /// ```
138 fn rest_endpoint(&self) -> &'static str;
139
140 /// Returns the WebSocket endpoint for a specific channel type.
141 ///
142 /// OKX uses different WebSocket URLs for different channel types:
143 /// - `Public`: `/ws/v5/public` - Market data
144 /// - `Private`: `/ws/v5/private` - Account data
145 /// - `Business`: `/ws/v5/business` - Trade execution
146 ///
147 /// In demo trading mode, the URLs switch to the demo domain
148 /// (`wspap.okx.com`) with `brokerId=9999` parameter.
149 ///
150 /// # Arguments
151 ///
152 /// * `channel_type` - The WebSocket channel type (Public, Private, Business)
153 ///
154 /// # Returns
155 ///
156 /// The complete WebSocket URL for the specified channel type.
157 ///
158 /// # Example
159 ///
160 /// ```rust,no_run
161 /// use ccxt_exchanges::okx::{Okx, OkxEndpointRouter, OkxChannelType};
162 /// use ccxt_core::ExchangeConfig;
163 ///
164 /// let okx = Okx::new(ExchangeConfig::default()).unwrap();
165 ///
166 /// // Get WebSocket URL for public market data
167 /// let ws_public = okx.ws_endpoint(OkxChannelType::Public);
168 /// assert!(ws_public.contains("/ws/v5/public"));
169 ///
170 /// // Get WebSocket URL for private account data
171 /// let ws_private = okx.ws_endpoint(OkxChannelType::Private);
172 /// assert!(ws_private.contains("/ws/v5/private"));
173 ///
174 /// // Get WebSocket URL for business/trading
175 /// let ws_business = okx.ws_endpoint(OkxChannelType::Business);
176 /// assert!(ws_business.contains("/ws/v5/business"));
177 /// ```
178 fn ws_endpoint(&self, channel_type: OkxChannelType) -> &str;
179
180 /// Returns whether demo trading mode is enabled.
181 ///
182 /// Demo trading mode is enabled when either:
183 /// - `config.sandbox` is set to `true`
184 /// - `options.testnet` is set to `true`
185 ///
186 /// When demo trading is enabled:
187 /// - REST requests should include the `x-simulated-trading: 1` header
188 /// - WebSocket URLs use the demo domain (`wspap.okx.com`)
189 ///
190 /// # Returns
191 ///
192 /// `true` if demo trading mode is enabled, `false` otherwise.
193 ///
194 /// # Example
195 ///
196 /// ```rust,no_run
197 /// use ccxt_exchanges::okx::{Okx, OkxEndpointRouter};
198 /// use ccxt_core::ExchangeConfig;
199 ///
200 /// // Production mode
201 /// let okx = Okx::new(ExchangeConfig::default()).unwrap();
202 /// assert!(!okx.is_demo_trading());
203 ///
204 /// // Demo mode
205 /// let config = ExchangeConfig {
206 /// sandbox: true,
207 /// ..Default::default()
208 /// };
209 /// let okx_demo = Okx::new(config).unwrap();
210 /// assert!(okx_demo.is_demo_trading());
211 /// ```
212 fn is_demo_trading(&self) -> bool;
213}
214
215use super::Okx;
216
217impl OkxEndpointRouter for Okx {
218 fn rest_endpoint(&self) -> &'static str {
219 // OKX uses the same REST domain for both production and demo trading.
220 // Demo mode is indicated by the `x-simulated-trading: 1` header,
221 // not by a different URL.
222 "https://www.okx.com"
223 }
224
225 fn ws_endpoint(&self, channel_type: OkxChannelType) -> &str {
226 // OKX WebSocket URLs differ between production and demo mode
227 if self.is_testnet_trading() {
228 // Demo trading WebSocket URLs
229 match channel_type {
230 OkxChannelType::Public => "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999",
231 OkxChannelType::Private => "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999",
232 OkxChannelType::Business => "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999",
233 }
234 } else {
235 // Production WebSocket URLs
236 match channel_type {
237 OkxChannelType::Public => "wss://ws.okx.com:8443/ws/v5/public",
238 OkxChannelType::Private => "wss://ws.okx.com:8443/ws/v5/private",
239 OkxChannelType::Business => "wss://ws.okx.com:8443/ws/v5/business",
240 }
241 }
242 }
243
244 fn is_demo_trading(&self) -> bool {
245 // Delegate to the existing method that checks both config.sandbox and options.testnet
246 self.is_testnet_trading()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::okx::OkxOptions;
254 use ccxt_core::ExchangeConfig;
255
256 fn create_test_okx() -> Okx {
257 Okx::new(ExchangeConfig::default()).unwrap()
258 }
259
260 fn create_demo_okx() -> Okx {
261 let config = ExchangeConfig {
262 sandbox: true,
263 ..Default::default()
264 };
265 Okx::new(config).unwrap()
266 }
267
268 // ==================== REST Endpoint Tests ====================
269
270 #[test]
271 fn test_rest_endpoint_production() {
272 let okx = create_test_okx();
273 let url = okx.rest_endpoint();
274 assert_eq!(url, "https://www.okx.com");
275 }
276
277 #[test]
278 fn test_rest_endpoint_demo() {
279 let okx = create_demo_okx();
280 let url = okx.rest_endpoint();
281 // OKX uses the same REST domain for demo trading
282 // Demo mode is indicated by header, not URL
283 assert_eq!(url, "https://www.okx.com");
284 }
285
286 // ==================== WebSocket Public Endpoint Tests ====================
287
288 #[test]
289 fn test_ws_endpoint_public_production() {
290 let okx = create_test_okx();
291 let url = okx.ws_endpoint(OkxChannelType::Public);
292 assert_eq!(url, "wss://ws.okx.com:8443/ws/v5/public");
293 assert!(!url.contains("brokerId"));
294 }
295
296 #[test]
297 fn test_ws_endpoint_public_demo() {
298 let okx = create_demo_okx();
299 let url = okx.ws_endpoint(OkxChannelType::Public);
300 assert!(url.contains("wspap.okx.com"));
301 assert!(url.contains("/ws/v5/public"));
302 assert!(url.contains("brokerId=9999"));
303 }
304
305 // ==================== WebSocket Private Endpoint Tests ====================
306
307 #[test]
308 fn test_ws_endpoint_private_production() {
309 let okx = create_test_okx();
310 let url = okx.ws_endpoint(OkxChannelType::Private);
311 assert_eq!(url, "wss://ws.okx.com:8443/ws/v5/private");
312 assert!(!url.contains("brokerId"));
313 }
314
315 #[test]
316 fn test_ws_endpoint_private_demo() {
317 let okx = create_demo_okx();
318 let url = okx.ws_endpoint(OkxChannelType::Private);
319 assert!(url.contains("wspap.okx.com"));
320 assert!(url.contains("/ws/v5/private"));
321 assert!(url.contains("brokerId=9999"));
322 }
323
324 // ==================== WebSocket Business Endpoint Tests ====================
325
326 #[test]
327 fn test_ws_endpoint_business_production() {
328 let okx = create_test_okx();
329 let url = okx.ws_endpoint(OkxChannelType::Business);
330 assert_eq!(url, "wss://ws.okx.com:8443/ws/v5/business");
331 assert!(!url.contains("brokerId"));
332 }
333
334 #[test]
335 fn test_ws_endpoint_business_demo() {
336 let okx = create_demo_okx();
337 let url = okx.ws_endpoint(OkxChannelType::Business);
338 assert!(url.contains("wspap.okx.com"));
339 assert!(url.contains("/ws/v5/business"));
340 assert!(url.contains("brokerId=9999"));
341 }
342
343 // ==================== Demo Trading Mode Tests ====================
344
345 #[test]
346 fn test_is_demo_trading_false_by_default() {
347 let okx = create_test_okx();
348 assert!(!okx.is_demo_trading());
349 }
350
351 #[test]
352 fn test_is_demo_trading_with_sandbox_config() {
353 let okx = create_demo_okx();
354 assert!(okx.is_demo_trading());
355 }
356
357 #[test]
358 fn test_is_demo_trading_with_testnet_option() {
359 let config = ExchangeConfig::default();
360 let options = OkxOptions {
361 testnet: true,
362 ..Default::default()
363 };
364 let okx = Okx::new_with_options(config, options).unwrap();
365 assert!(okx.is_demo_trading());
366 }
367
368 // ==================== Channel Type Display Tests ====================
369
370 #[test]
371 fn test_channel_type_display() {
372 assert_eq!(format!("{}", OkxChannelType::Public), "public");
373 assert_eq!(format!("{}", OkxChannelType::Private), "private");
374 assert_eq!(format!("{}", OkxChannelType::Business), "business");
375 }
376
377 // ==================== Channel Type Equality Tests ====================
378
379 #[test]
380 fn test_channel_type_equality() {
381 assert_eq!(OkxChannelType::Public, OkxChannelType::Public);
382 assert_eq!(OkxChannelType::Private, OkxChannelType::Private);
383 assert_eq!(OkxChannelType::Business, OkxChannelType::Business);
384 assert_ne!(OkxChannelType::Public, OkxChannelType::Private);
385 assert_ne!(OkxChannelType::Private, OkxChannelType::Business);
386 }
387
388 // ==================== All Channel Types Tests ====================
389
390 #[test]
391 fn test_all_channel_types_production() {
392 let okx = create_test_okx();
393
394 let channels = [
395 (OkxChannelType::Public, "/ws/v5/public"),
396 (OkxChannelType::Private, "/ws/v5/private"),
397 (OkxChannelType::Business, "/ws/v5/business"),
398 ];
399
400 for (channel_type, expected_path) in channels {
401 let url = okx.ws_endpoint(channel_type);
402 assert!(
403 url.contains(expected_path),
404 "URL {} should contain {}",
405 url,
406 expected_path
407 );
408 assert!(
409 url.contains("ws.okx.com"),
410 "Production URL {} should contain ws.okx.com",
411 url
412 );
413 }
414 }
415
416 #[test]
417 fn test_all_channel_types_demo() {
418 let okx = create_demo_okx();
419
420 let channels = [
421 (OkxChannelType::Public, "/ws/v5/public"),
422 (OkxChannelType::Private, "/ws/v5/private"),
423 (OkxChannelType::Business, "/ws/v5/business"),
424 ];
425
426 for (channel_type, expected_path) in channels {
427 let url = okx.ws_endpoint(channel_type);
428 assert!(
429 url.contains(expected_path),
430 "URL {} should contain {}",
431 url,
432 expected_path
433 );
434 assert!(
435 url.contains("wspap.okx.com"),
436 "Demo URL {} should contain wspap.okx.com",
437 url
438 );
439 assert!(
440 url.contains("brokerId=9999"),
441 "Demo URL {} should contain brokerId=9999",
442 url
443 );
444 }
445 }
446
447 // ==================== Consistency Tests ====================
448
449 #[test]
450 fn test_rest_endpoint_same_for_production_and_demo() {
451 let okx_prod = create_test_okx();
452 let okx_demo = create_demo_okx();
453
454 // OKX uses the same REST domain for both modes
455 assert_eq!(okx_prod.rest_endpoint(), okx_demo.rest_endpoint());
456 }
457
458 #[test]
459 fn test_ws_endpoints_differ_for_production_and_demo() {
460 let okx_prod = create_test_okx();
461 let okx_demo = create_demo_okx();
462
463 // WebSocket URLs should be different
464 assert_ne!(
465 okx_prod.ws_endpoint(OkxChannelType::Public),
466 okx_demo.ws_endpoint(OkxChannelType::Public)
467 );
468 assert_ne!(
469 okx_prod.ws_endpoint(OkxChannelType::Private),
470 okx_demo.ws_endpoint(OkxChannelType::Private)
471 );
472 assert_ne!(
473 okx_prod.ws_endpoint(OkxChannelType::Business),
474 okx_demo.ws_endpoint(OkxChannelType::Business)
475 );
476 }
477}