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 rust_decimal::prelude::ToPrimitive;
16use std::collections::HashMap;
17
18use super::Okx;
19
20#[async_trait]
21impl Exchange for Okx {
22    // ==================== Metadata ====================
23
24    fn id(&self) -> &str {
25        "okx"
26    }
27
28    fn name(&self) -> &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.into_values().map(|m| (*m).clone()).collect())
96    }
97
98    async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
99        let arc_markets = Okx::load_markets(self, reload).await?;
100        Ok(arc_markets
101            .into_iter()
102            .map(|(k, v)| (k, (*v).clone()))
103            .collect())
104    }
105
106    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
107        Okx::fetch_ticker(self, symbol).await
108    }
109
110    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
111        let symbols_vec = symbols.map(|s| s.to_vec());
112        Okx::fetch_tickers(self, symbols_vec).await
113    }
114
115    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
116        Okx::fetch_order_book(self, symbol, limit).await
117    }
118
119    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
120        Okx::fetch_trades(self, symbol, limit).await
121    }
122
123    async fn fetch_ohlcv(
124        &self,
125        symbol: &str,
126        timeframe: Timeframe,
127        since: Option<i64>,
128        limit: Option<u32>,
129    ) -> Result<Vec<Ohlcv>> {
130        let timeframe_str = timeframe.to_string();
131        let ohlcv_data = Okx::fetch_ohlcv(self, symbol, &timeframe_str, since, limit).await?;
132
133        // Convert OHLCV to Ohlcv with proper type conversions
134        Ok(ohlcv_data
135            .into_iter()
136            .map(|o| Ohlcv {
137                timestamp: o.timestamp,
138                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
139                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
140                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
141                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
142                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
143            })
144            .collect())
145    }
146
147    // ==================== Trading (Private API) ====================
148
149    async fn create_order(
150        &self,
151        symbol: &str,
152        order_type: OrderType,
153        side: OrderSide,
154        amount: Decimal,
155        price: Option<Decimal>,
156    ) -> Result<Order> {
157        let amount_f64 = amount
158            .to_f64()
159            .ok_or_else(|| ccxt_core::Error::invalid_request("Failed to convert amount to f64"))?;
160        let price_f64 = match price {
161            Some(p) => Some(p.to_f64().ok_or_else(|| {
162                ccxt_core::Error::invalid_request("Failed to convert price to f64")
163            })?),
164            None => None,
165        };
166
167        Okx::create_order(self, symbol, order_type, side, amount_f64, price_f64).await
168    }
169
170    async fn cancel_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 cancel_order on OKX")
173        })?;
174        Okx::cancel_order(self, id, symbol_str).await
175    }
176
177    async fn cancel_all_orders(&self, _symbol: Option<&str>) -> Result<Vec<Order>> {
178        Err(ccxt_core::Error::not_implemented("cancel_all_orders"))
179    }
180
181    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
182        let symbol_str = symbol.ok_or_else(|| {
183            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on OKX")
184        })?;
185        Okx::fetch_order(self, id, symbol_str).await
186    }
187
188    async fn fetch_open_orders(
189        &self,
190        symbol: Option<&str>,
191        since: Option<i64>,
192        limit: Option<u32>,
193    ) -> Result<Vec<Order>> {
194        Okx::fetch_open_orders(self, symbol, since, limit).await
195    }
196
197    async fn fetch_closed_orders(
198        &self,
199        symbol: Option<&str>,
200        since: Option<i64>,
201        limit: Option<u32>,
202    ) -> Result<Vec<Order>> {
203        Okx::fetch_closed_orders(self, symbol, since, limit).await
204    }
205
206    // ==================== Account (Private API) ====================
207
208    async fn fetch_balance(&self) -> Result<Balance> {
209        Okx::fetch_balance(self).await
210    }
211
212    async fn fetch_my_trades(
213        &self,
214        symbol: Option<&str>,
215        since: Option<i64>,
216        limit: Option<u32>,
217    ) -> Result<Vec<Trade>> {
218        let symbol_str = symbol.ok_or_else(|| {
219            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on OKX")
220        })?;
221        Okx::fetch_my_trades(self, symbol_str, since, limit).await
222    }
223
224    // ==================== Helper Methods ====================
225
226    async fn market(&self, symbol: &str) -> Result<Market> {
227        let cache = self.base().market_cache.read().await;
228
229        if !cache.loaded {
230            return Err(ccxt_core::Error::exchange(
231                "-1",
232                "Markets not loaded. Call load_markets() first.",
233            ));
234        }
235
236        cache
237            .markets
238            .get(symbol)
239            .map(|m| (**m).clone())
240            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
241    }
242
243    async fn markets(&self) -> HashMap<String, Market> {
244        let cache = self.base().market_cache.read().await;
245        cache
246            .markets
247            .iter()
248            .map(|(k, v)| (k.clone(), (**v).clone()))
249            .collect()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use ccxt_core::ExchangeConfig;
257
258    #[test]
259    fn test_okx_exchange_trait_metadata() {
260        let config = ExchangeConfig::default();
261        let okx = Okx::new(config).unwrap();
262
263        // Test metadata methods via Exchange trait
264        let exchange: &dyn Exchange = &okx;
265
266        assert_eq!(exchange.id(), "okx");
267        assert_eq!(exchange.name(), "OKX");
268        assert_eq!(exchange.version(), "v5");
269        assert!(!exchange.certified());
270        assert!(exchange.has_websocket());
271    }
272
273    #[test]
274    fn test_okx_exchange_trait_capabilities() {
275        let config = ExchangeConfig::default();
276        let okx = Okx::new(config).unwrap();
277
278        let exchange: &dyn Exchange = &okx;
279        let caps = exchange.capabilities();
280
281        // Public API capabilities
282        assert!(caps.fetch_markets());
283        assert!(caps.fetch_ticker());
284        assert!(caps.fetch_tickers());
285        assert!(caps.fetch_order_book());
286        assert!(caps.fetch_trades());
287        assert!(caps.fetch_ohlcv());
288
289        // Private API capabilities
290        assert!(caps.create_order());
291        assert!(caps.cancel_order());
292        assert!(caps.fetch_order());
293        assert!(caps.fetch_open_orders());
294        assert!(caps.fetch_closed_orders());
295        assert!(caps.fetch_balance());
296        assert!(caps.fetch_my_trades());
297
298        // WebSocket capabilities
299        assert!(caps.websocket());
300        assert!(caps.watch_ticker());
301        assert!(caps.watch_order_book());
302        assert!(caps.watch_trades());
303
304        // Not implemented capabilities
305        assert!(!caps.edit_order());
306        assert!(!caps.cancel_all_orders());
307        assert!(!caps.fetch_currencies());
308    }
309
310    #[test]
311    fn test_okx_exchange_trait_timeframes() {
312        let config = ExchangeConfig::default();
313        let okx = Okx::new(config).unwrap();
314
315        let exchange: &dyn Exchange = &okx;
316        let timeframes = exchange.timeframes();
317
318        assert!(!timeframes.is_empty());
319        assert!(timeframes.contains(&Timeframe::M1));
320        assert!(timeframes.contains(&Timeframe::M3));
321        assert!(timeframes.contains(&Timeframe::M5));
322        assert!(timeframes.contains(&Timeframe::M15));
323        assert!(timeframes.contains(&Timeframe::H1));
324        assert!(timeframes.contains(&Timeframe::H4));
325        assert!(timeframes.contains(&Timeframe::D1));
326        assert!(timeframes.contains(&Timeframe::W1));
327        assert!(timeframes.contains(&Timeframe::Mon1));
328    }
329
330    #[test]
331    fn test_okx_exchange_trait_rate_limit() {
332        let config = ExchangeConfig::default();
333        let okx = Okx::new(config).unwrap();
334
335        let exchange: &dyn Exchange = &okx;
336        assert_eq!(exchange.rate_limit(), 20);
337    }
338
339    #[test]
340    fn test_okx_exchange_trait_object_safety() {
341        let config = ExchangeConfig::default();
342        let okx = Okx::new(config).unwrap();
343
344        // Test that we can create a trait object (Box<dyn Exchange>)
345        let exchange: Box<dyn Exchange> = Box::new(okx);
346
347        assert_eq!(exchange.id(), "okx");
348        assert_eq!(exchange.name(), "OKX");
349        assert_eq!(exchange.rate_limit(), 20);
350    }
351
352    #[test]
353    fn test_okx_exchange_trait_polymorphic_usage() {
354        let config = ExchangeConfig::default();
355        let okx = Okx::new(config).unwrap();
356
357        // Test polymorphic usage with &dyn Exchange
358        fn check_exchange_metadata(exchange: &dyn Exchange) -> (&str, &str, bool) {
359            (exchange.id(), exchange.name(), exchange.has_websocket())
360        }
361
362        let (id, name, has_ws) = check_exchange_metadata(&okx);
363        assert_eq!(id, "okx");
364        assert_eq!(name, "OKX");
365        assert!(has_ws);
366    }
367
368    #[test]
369    fn test_okx_capabilities_has_method() {
370        let config = ExchangeConfig::default();
371        let okx = Okx::new(config).unwrap();
372
373        let exchange: &dyn Exchange = &okx;
374        let caps = exchange.capabilities();
375
376        // Test the has() method with CCXT-style camelCase names
377        assert!(caps.has("fetchMarkets"));
378        assert!(caps.has("fetchTicker"));
379        assert!(caps.has("fetchTickers"));
380        assert!(caps.has("fetchOrderBook"));
381        assert!(caps.has("fetchTrades"));
382        assert!(caps.has("fetchOHLCV"));
383        assert!(caps.has("createOrder"));
384        assert!(caps.has("cancelOrder"));
385        assert!(caps.has("fetchOrder"));
386        assert!(caps.has("fetchOpenOrders"));
387        assert!(caps.has("fetchClosedOrders"));
388        assert!(caps.has("fetchBalance"));
389        assert!(caps.has("fetchMyTrades"));
390        assert!(caps.has("websocket"));
391
392        // Not implemented
393        assert!(!caps.has("editOrder"));
394        assert!(!caps.has("cancelAllOrders"));
395        assert!(!caps.has("fetchCurrencies"));
396        assert!(!caps.has("unknownCapability"));
397    }
398}