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