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