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}