1use 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 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 ExchangeCapabilities::builder()
47 .market_data()
49 .without_capability(Capability::FetchCurrencies)
50 .without_capability(Capability::FetchStatus)
51 .without_capability(Capability::FetchTime)
52 .trading()
54 .without_capability(Capability::CancelAllOrders)
55 .without_capability(Capability::EditOrder)
56 .without_capability(Capability::FetchOrders)
57 .without_capability(Capability::FetchCanceledOrders)
58 .capability(Capability::FetchBalance)
60 .capability(Capability::FetchMyTrades)
61 .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 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 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 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 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 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 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 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 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 assert!(caps.websocket());
300 assert!(caps.watch_ticker());
301 assert!(caps.watch_order_book());
302 assert!(caps.watch_trades());
303
304 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 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 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 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 assert!(!caps.has("editOrder"));
394 assert!(!caps.has("cancelAllOrders"));
395 assert!(!caps.has("fetchCurrencies"));
396 assert!(!caps.has("unknownCapability"));
397 }
398}