ccxt_exchanges/okx/
mod.rs

1//! OKX exchange implementation.
2//!
3//! Supports spot trading and futures trading (USDT-M and Coin-M) with REST API and WebSocket support.
4//! OKX uses V5 unified API with HMAC-SHA256 + Base64 authentication.
5
6use ccxt_core::{BaseExchange, ExchangeConfig, Result};
7use std::collections::HashMap;
8
9pub mod auth;
10pub mod builder;
11pub mod error;
12pub mod exchange_impl;
13pub mod parser;
14pub mod rest;
15pub mod symbol;
16pub mod ws;
17pub mod ws_exchange_impl;
18
19pub use auth::OkxAuth;
20pub use builder::OkxBuilder;
21pub use error::{OkxErrorCode, is_error_response, parse_error};
22
23/// OKX exchange structure.
24#[derive(Debug)]
25pub struct Okx {
26    /// Base exchange instance.
27    base: BaseExchange,
28    /// OKX-specific options.
29    options: OkxOptions,
30}
31
32/// OKX-specific options.
33#[derive(Debug, Clone)]
34pub struct OkxOptions {
35    /// Account mode: cash (spot), cross (cross margin), isolated (isolated margin).
36    pub account_mode: String,
37    /// Enables demo trading environment.
38    pub demo: bool,
39}
40
41impl Default for OkxOptions {
42    fn default() -> Self {
43        Self {
44            account_mode: "cash".to_string(),
45            demo: false,
46        }
47    }
48}
49
50impl Okx {
51    /// Creates a new OKX instance using the builder pattern.
52    ///
53    /// This is the recommended way to create an OKX instance.
54    ///
55    /// # Example
56    ///
57    /// ```no_run
58    /// use ccxt_exchanges::okx::Okx;
59    ///
60    /// let okx = Okx::builder()
61    ///     .api_key("your-api-key")
62    ///     .secret("your-secret")
63    ///     .passphrase("your-passphrase")
64    ///     .sandbox(true)
65    ///     .build()
66    ///     .unwrap();
67    /// ```
68    pub fn builder() -> OkxBuilder {
69        OkxBuilder::new()
70    }
71
72    /// Creates a new OKX instance.
73    ///
74    /// # Arguments
75    ///
76    /// * `config` - Exchange configuration.
77    pub fn new(config: ExchangeConfig) -> Result<Self> {
78        let base = BaseExchange::new(config)?;
79        let options = OkxOptions::default();
80
81        Ok(Self { base, options })
82    }
83
84    /// Creates a new OKX instance with custom options.
85    ///
86    /// This is used internally by the builder pattern.
87    ///
88    /// # Arguments
89    ///
90    /// * `config` - Exchange configuration.
91    /// * `options` - OKX-specific options.
92    pub fn new_with_options(config: ExchangeConfig, options: OkxOptions) -> Result<Self> {
93        let base = BaseExchange::new(config)?;
94        Ok(Self { base, options })
95    }
96
97    /// Returns a reference to the base exchange.
98    pub fn base(&self) -> &BaseExchange {
99        &self.base
100    }
101
102    /// Returns a mutable reference to the base exchange.
103    pub fn base_mut(&mut self) -> &mut BaseExchange {
104        &mut self.base
105    }
106
107    /// Returns the OKX options.
108    pub fn options(&self) -> &OkxOptions {
109        &self.options
110    }
111
112    /// Sets the OKX options.
113    pub fn set_options(&mut self, options: OkxOptions) {
114        self.options = options;
115    }
116
117    /// Returns the exchange ID.
118    pub fn id(&self) -> &str {
119        "okx"
120    }
121
122    /// Returns the exchange name.
123    pub fn name(&self) -> &str {
124        "OKX"
125    }
126
127    /// Returns the API version.
128    pub fn version(&self) -> &str {
129        "v5"
130    }
131
132    /// Returns `true` if the exchange is CCXT-certified.
133    pub fn certified(&self) -> bool {
134        false
135    }
136
137    /// Returns `true` if Pro version (WebSocket) is supported.
138    pub fn pro(&self) -> bool {
139        true
140    }
141
142    /// Returns the rate limit in requests per second.
143    pub fn rate_limit(&self) -> f64 {
144        20.0
145    }
146
147    /// Returns the supported timeframes.
148    pub fn timeframes(&self) -> HashMap<String, String> {
149        let mut timeframes = HashMap::new();
150        timeframes.insert("1m".to_string(), "1m".to_string());
151        timeframes.insert("3m".to_string(), "3m".to_string());
152        timeframes.insert("5m".to_string(), "5m".to_string());
153        timeframes.insert("15m".to_string(), "15m".to_string());
154        timeframes.insert("30m".to_string(), "30m".to_string());
155        timeframes.insert("1h".to_string(), "1H".to_string());
156        timeframes.insert("2h".to_string(), "2H".to_string());
157        timeframes.insert("4h".to_string(), "4H".to_string());
158        timeframes.insert("6h".to_string(), "6Hutc".to_string());
159        timeframes.insert("12h".to_string(), "12Hutc".to_string());
160        timeframes.insert("1d".to_string(), "1Dutc".to_string());
161        timeframes.insert("1w".to_string(), "1Wutc".to_string());
162        timeframes.insert("1M".to_string(), "1Mutc".to_string());
163        timeframes
164    }
165
166    /// Returns the API URLs.
167    pub fn urls(&self) -> OkxUrls {
168        if self.base().config.sandbox || self.options.demo {
169            OkxUrls::demo()
170        } else {
171            OkxUrls::production()
172        }
173    }
174
175    /// Creates a new WebSocket client for OKX.
176    ///
177    /// Returns an `OkxWs` instance configured with the appropriate WebSocket URL
178    /// based on the exchange configuration (production or demo).
179    ///
180    /// # Example
181    /// ```no_run
182    /// use ccxt_exchanges::okx::Okx;
183    /// use ccxt_core::ExchangeConfig;
184    ///
185    /// # async fn example() -> ccxt_core::error::Result<()> {
186    /// let okx = Okx::new(ExchangeConfig::default())?;
187    /// let ws = okx.create_ws();
188    /// ws.connect().await?;
189    /// # Ok(())
190    /// # }
191    /// ```
192    pub fn create_ws(&self) -> ws::OkxWs {
193        let urls = self.urls();
194        ws::OkxWs::new(urls.ws_public)
195    }
196}
197
198/// OKX API URLs.
199#[derive(Debug, Clone)]
200pub struct OkxUrls {
201    /// REST API base URL.
202    pub rest: String,
203    /// Public WebSocket URL.
204    pub ws_public: String,
205    /// Private WebSocket URL.
206    pub ws_private: String,
207    /// Business WebSocket URL.
208    pub ws_business: String,
209}
210
211impl OkxUrls {
212    /// Returns production environment URLs.
213    pub fn production() -> Self {
214        Self {
215            rest: "https://www.okx.com".to_string(),
216            ws_public: "wss://ws.okx.com:8443/ws/v5/public".to_string(),
217            ws_private: "wss://ws.okx.com:8443/ws/v5/private".to_string(),
218            ws_business: "wss://ws.okx.com:8443/ws/v5/business".to_string(),
219        }
220    }
221
222    /// Returns demo environment URLs.
223    pub fn demo() -> Self {
224        Self {
225            rest: "https://www.okx.com".to_string(),
226            ws_public: "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999".to_string(),
227            ws_private: "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999".to_string(),
228            ws_business: "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999".to_string(),
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_okx_creation() {
239        let config = ExchangeConfig {
240            id: "okx".to_string(),
241            name: "OKX".to_string(),
242            ..Default::default()
243        };
244
245        let okx = Okx::new(config);
246        assert!(okx.is_ok());
247
248        let okx = okx.unwrap();
249        assert_eq!(okx.id(), "okx");
250        assert_eq!(okx.name(), "OKX");
251        assert_eq!(okx.version(), "v5");
252        assert!(!okx.certified());
253        assert!(okx.pro());
254    }
255
256    #[test]
257    fn test_timeframes() {
258        let config = ExchangeConfig::default();
259        let okx = Okx::new(config).unwrap();
260        let timeframes = okx.timeframes();
261
262        assert!(timeframes.contains_key("1m"));
263        assert!(timeframes.contains_key("1h"));
264        assert!(timeframes.contains_key("1d"));
265        assert_eq!(timeframes.len(), 13);
266    }
267
268    #[test]
269    fn test_urls() {
270        let config = ExchangeConfig::default();
271        let okx = Okx::new(config).unwrap();
272        let urls = okx.urls();
273
274        assert!(urls.rest.contains("okx.com"));
275        assert!(urls.ws_public.contains("ws.okx.com"));
276    }
277
278    #[test]
279    fn test_sandbox_urls() {
280        let config = ExchangeConfig {
281            sandbox: true,
282            ..Default::default()
283        };
284        let okx = Okx::new(config).unwrap();
285        let urls = okx.urls();
286
287        assert!(urls.ws_public.contains("wspap.okx.com"));
288        assert!(urls.ws_public.contains("brokerId=9999"));
289    }
290
291    #[test]
292    fn test_default_options() {
293        let options = OkxOptions::default();
294        assert_eq!(options.account_mode, "cash");
295        assert!(!options.demo);
296    }
297}