ccxt_exchanges/okx/
exchange_impl.rs

1//! Exchange trait implementation for OKX
2//!
3//! This module implements the unified `Exchange` trait from `ccxt-core` for OKX.
4
5use async_trait::async_trait;
6use ccxt_core::{
7    Result,
8    exchange::{Capability, Exchange, ExchangeCapabilities},
9    signed_request::HasHttpClient,
10    types::{
11        Amount, Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Price, Ticker,
12        Timeframe, Trade,
13    },
14};
15use rust_decimal::Decimal;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use super::Okx;
20
21// ==================== HasHttpClient Implementation ====================
22
23impl HasHttpClient for Okx {
24    fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
25        &self.base().http_client
26    }
27
28    fn base_url(&self) -> &'static str {
29        // Return empty string - OKX's signed_request builder handles full URL construction
30        // using self.okx.urls().rest
31        ""
32    }
33}
34
35#[async_trait]
36impl Exchange for Okx {
37    // ==================== Metadata ====================
38
39    fn id(&self) -> &'static str {
40        "okx"
41    }
42
43    fn name(&self) -> &'static str {
44        "OKX"
45    }
46
47    fn version(&self) -> &'static str {
48        "v5"
49    }
50
51    fn certified(&self) -> bool {
52        false
53    }
54
55    fn has_websocket(&self) -> bool {
56        true
57    }
58
59    fn capabilities(&self) -> ExchangeCapabilities {
60        // OKX capabilities: market data + trading (with some exclusions) + limited account + limited websocket
61        ExchangeCapabilities::builder()
62            // Market Data: all except fetch_currencies, fetch_status, fetch_time
63            .market_data()
64            .without_capability(Capability::FetchCurrencies)
65            .without_capability(Capability::FetchStatus)
66            .without_capability(Capability::FetchTime)
67            // Trading: all except cancel_all_orders, edit_order, fetch_orders, fetch_canceled_orders
68            .trading()
69            .without_capability(Capability::CancelAllOrders)
70            .without_capability(Capability::EditOrder)
71            .without_capability(Capability::FetchOrders)
72            .without_capability(Capability::FetchCanceledOrders)
73            // Account: only fetch_balance and fetch_my_trades
74            .capability(Capability::FetchBalance)
75            .capability(Capability::FetchMyTrades)
76            // WebSocket: websocket, watch_ticker, watch_order_book, watch_trades
77            .capability(Capability::Websocket)
78            .capability(Capability::WatchTicker)
79            .capability(Capability::WatchOrderBook)
80            .capability(Capability::WatchTrades)
81            .build()
82    }
83
84    fn timeframes(&self) -> Vec<Timeframe> {
85        vec![
86            Timeframe::M1,
87            Timeframe::M3,
88            Timeframe::M5,
89            Timeframe::M15,
90            Timeframe::M30,
91            Timeframe::H1,
92            Timeframe::H2,
93            Timeframe::H4,
94            Timeframe::H6,
95            Timeframe::H12,
96            Timeframe::D1,
97            Timeframe::W1,
98            Timeframe::Mon1,
99        ]
100    }
101
102    fn rate_limit(&self) -> u32 {
103        20
104    }
105
106    // ==================== Market Data (Public API) ====================
107
108    async fn fetch_markets(&self) -> Result<Vec<Market>> {
109        let markets = Okx::fetch_markets(self).await?;
110        Ok(markets.values().map(|m| (**m).clone()).collect())
111    }
112
113    async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
114        Okx::load_markets(self, reload).await
115    }
116
117    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
118        Okx::fetch_ticker(self, symbol).await
119    }
120
121    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
122        let symbols_vec = symbols.map(<[String]>::to_vec);
123        Okx::fetch_tickers(self, symbols_vec).await
124    }
125
126    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
127        Okx::fetch_order_book(self, symbol, limit).await
128    }
129
130    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
131        Okx::fetch_trades(self, symbol, limit).await
132    }
133
134    async fn fetch_ohlcv(
135        &self,
136        symbol: &str,
137        timeframe: Timeframe,
138        since: Option<i64>,
139        limit: Option<u32>,
140    ) -> Result<Vec<Ohlcv>> {
141        let timeframe_str = timeframe.to_string();
142        #[allow(deprecated)]
143        let ohlcv_data = Okx::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;
144
145        // Convert OHLCV to Ohlcv with proper type conversions
146        ohlcv_data
147            .into_iter()
148            .map(|o| -> ccxt_core::Result<Ohlcv> {
149                Ok(Ohlcv {
150                    timestamp: o.timestamp,
151                    open: Price(Decimal::try_from(o.open).map_err(|e| {
152                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
153                            "OHLCV open",
154                            format!("{e}"),
155                        ))
156                    })?),
157                    high: Price(Decimal::try_from(o.high).map_err(|e| {
158                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
159                            "OHLCV high",
160                            format!("{e}"),
161                        ))
162                    })?),
163                    low: Price(Decimal::try_from(o.low).map_err(|e| {
164                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
165                            "OHLCV low",
166                            format!("{e}"),
167                        ))
168                    })?),
169                    close: Price(Decimal::try_from(o.close).map_err(|e| {
170                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
171                            "OHLCV close",
172                            format!("{e}"),
173                        ))
174                    })?),
175                    volume: Amount(Decimal::try_from(o.volume).map_err(|e| {
176                        ccxt_core::Error::from(ccxt_core::ParseError::invalid_value(
177                            "OHLCV volume",
178                            format!("{e}"),
179                        ))
180                    })?),
181                })
182            })
183            .collect::<ccxt_core::Result<Vec<Ohlcv>>>()
184            .map_err(|e| e.context("Failed to convert OKX OHLCV data"))
185    }
186
187    // ==================== Trading (Private API) ====================
188
189    async fn create_order(
190        &self,
191        symbol: &str,
192        order_type: OrderType,
193        side: OrderSide,
194        amount: Amount,
195        price: Option<Price>,
196    ) -> Result<Order> {
197        // Direct delegation - no type conversion needed
198        #[allow(deprecated)]
199        Okx::create_order(self, symbol, order_type, side, amount, price).await
200    }
201
202    async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
203        let symbol_str = symbol.ok_or_else(|| {
204            ccxt_core::Error::invalid_request("Symbol is required for cancel_order on OKX")
205        })?;
206        Okx::cancel_order(self, id, symbol_str).await
207    }
208
209    async fn cancel_all_orders(&self, _symbol: Option<&str>) -> Result<Vec<Order>> {
210        Err(ccxt_core::Error::not_implemented("cancel_all_orders"))
211    }
212
213    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
214        let symbol_str = symbol.ok_or_else(|| {
215            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on OKX")
216        })?;
217        Okx::fetch_order(self, id, symbol_str).await
218    }
219
220    async fn fetch_open_orders(
221        &self,
222        symbol: Option<&str>,
223        since: Option<i64>,
224        limit: Option<u32>,
225    ) -> Result<Vec<Order>> {
226        Okx::fetch_open_orders(self, symbol, since, limit).await
227    }
228
229    async fn fetch_closed_orders(
230        &self,
231        symbol: Option<&str>,
232        since: Option<i64>,
233        limit: Option<u32>,
234    ) -> Result<Vec<Order>> {
235        Okx::fetch_closed_orders(self, symbol, since, limit).await
236    }
237
238    // ==================== Account (Private API) ====================
239
240    async fn fetch_balance(&self) -> Result<Balance> {
241        Okx::fetch_balance(self).await
242    }
243
244    async fn fetch_my_trades(
245        &self,
246        symbol: Option<&str>,
247        since: Option<i64>,
248        limit: Option<u32>,
249    ) -> Result<Vec<Trade>> {
250        let symbol_str = symbol.ok_or_else(|| {
251            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on OKX")
252        })?;
253        Okx::fetch_my_trades(self, symbol_str, since, limit).await
254    }
255
256    // ==================== Helper Methods ====================
257
258    async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
259        let cache = self.base().market_cache.read().await;
260
261        if !cache.is_loaded() {
262            return Err(ccxt_core::Error::exchange(
263                "-1",
264                "Markets not loaded. Call load_markets() first.",
265            ));
266        }
267
268        cache
269            .get_market(symbol)
270            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
271    }
272
273    async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
274        let cache = self.base().market_cache.read().await;
275        cache.markets()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use ccxt_core::ExchangeConfig;
283
284    #[test]
285    fn test_okx_exchange_trait_metadata() {
286        let config = ExchangeConfig::default();
287        let okx = Okx::new(config).unwrap();
288
289        // Test metadata methods via Exchange trait
290        let exchange: &dyn Exchange = &okx;
291
292        assert_eq!(exchange.id(), "okx");
293        assert_eq!(exchange.name(), "OKX");
294        assert_eq!(exchange.version(), "v5");
295        assert!(!exchange.certified());
296        assert!(exchange.has_websocket());
297    }
298
299    #[test]
300    fn test_okx_exchange_trait_capabilities() {
301        let config = ExchangeConfig::default();
302        let okx = Okx::new(config).unwrap();
303
304        let exchange: &dyn Exchange = &okx;
305        let caps = exchange.capabilities();
306
307        // Public API capabilities
308        assert!(caps.fetch_markets());
309        assert!(caps.fetch_ticker());
310        assert!(caps.fetch_tickers());
311        assert!(caps.fetch_order_book());
312        assert!(caps.fetch_trades());
313        assert!(caps.fetch_ohlcv());
314
315        // Private API capabilities
316        assert!(caps.create_order());
317        assert!(caps.cancel_order());
318        assert!(caps.fetch_order());
319        assert!(caps.fetch_open_orders());
320        assert!(caps.fetch_closed_orders());
321        assert!(caps.fetch_balance());
322        assert!(caps.fetch_my_trades());
323
324        // WebSocket capabilities
325        assert!(caps.websocket());
326        assert!(caps.watch_ticker());
327        assert!(caps.watch_order_book());
328        assert!(caps.watch_trades());
329
330        // Not implemented capabilities
331        assert!(!caps.edit_order());
332        assert!(!caps.cancel_all_orders());
333        assert!(!caps.fetch_currencies());
334    }
335
336    #[test]
337    fn test_okx_exchange_trait_timeframes() {
338        let config = ExchangeConfig::default();
339        let okx = Okx::new(config).unwrap();
340
341        let exchange: &dyn Exchange = &okx;
342        let timeframes = exchange.timeframes();
343
344        assert!(!timeframes.is_empty());
345        assert!(timeframes.contains(&Timeframe::M1));
346        assert!(timeframes.contains(&Timeframe::M3));
347        assert!(timeframes.contains(&Timeframe::M5));
348        assert!(timeframes.contains(&Timeframe::M15));
349        assert!(timeframes.contains(&Timeframe::H1));
350        assert!(timeframes.contains(&Timeframe::H4));
351        assert!(timeframes.contains(&Timeframe::D1));
352        assert!(timeframes.contains(&Timeframe::W1));
353        assert!(timeframes.contains(&Timeframe::Mon1));
354    }
355
356    #[test]
357    fn test_okx_exchange_trait_rate_limit() {
358        let config = ExchangeConfig::default();
359        let okx = Okx::new(config).unwrap();
360
361        let exchange: &dyn Exchange = &okx;
362        assert_eq!(exchange.rate_limit(), 20);
363    }
364
365    #[test]
366    fn test_okx_exchange_trait_object_safety() {
367        let config = ExchangeConfig::default();
368        let okx = Okx::new(config).unwrap();
369
370        // Test that we can create a trait object (Box<dyn Exchange>)
371        let exchange: Box<dyn Exchange> = Box::new(okx);
372
373        assert_eq!(exchange.id(), "okx");
374        assert_eq!(exchange.name(), "OKX");
375        assert_eq!(exchange.rate_limit(), 20);
376    }
377
378    #[test]
379    fn test_okx_exchange_trait_polymorphic_usage() {
380        let config = ExchangeConfig::default();
381        let okx = Okx::new(config).unwrap();
382
383        // Test polymorphic usage with &dyn Exchange
384        fn check_exchange_metadata(exchange: &dyn Exchange) -> (&str, &str, bool) {
385            (exchange.id(), exchange.name(), exchange.has_websocket())
386        }
387
388        let (id, name, has_ws) = check_exchange_metadata(&okx);
389        assert_eq!(id, "okx");
390        assert_eq!(name, "OKX");
391        assert!(has_ws);
392    }
393
394    #[test]
395    fn test_okx_capabilities_has_method() {
396        let config = ExchangeConfig::default();
397        let okx = Okx::new(config).unwrap();
398
399        let exchange: &dyn Exchange = &okx;
400        let caps = exchange.capabilities();
401
402        // Test the has() method with CCXT-style camelCase names
403        assert!(caps.has("fetchMarkets"));
404        assert!(caps.has("fetchTicker"));
405        assert!(caps.has("fetchTickers"));
406        assert!(caps.has("fetchOrderBook"));
407        assert!(caps.has("fetchTrades"));
408        assert!(caps.has("fetchOHLCV"));
409        assert!(caps.has("createOrder"));
410        assert!(caps.has("cancelOrder"));
411        assert!(caps.has("fetchOrder"));
412        assert!(caps.has("fetchOpenOrders"));
413        assert!(caps.has("fetchClosedOrders"));
414        assert!(caps.has("fetchBalance"));
415        assert!(caps.has("fetchMyTrades"));
416        assert!(caps.has("websocket"));
417
418        // Not implemented
419        assert!(!caps.has("editOrder"));
420        assert!(!caps.has("cancelAllOrders"));
421        assert!(!caps.has("fetchCurrencies"));
422        assert!(!caps.has("unknownCapability"));
423    }
424}