1use 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
21impl 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 ""
32 }
33}
34
35#[async_trait]
36impl Exchange for Okx {
37 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 ExchangeCapabilities::builder()
62 .market_data()
64 .without_capability(Capability::FetchCurrencies)
65 .without_capability(Capability::FetchStatus)
66 .without_capability(Capability::FetchTime)
67 .trading()
69 .without_capability(Capability::CancelAllOrders)
70 .without_capability(Capability::EditOrder)
71 .without_capability(Capability::FetchOrders)
72 .without_capability(Capability::FetchCanceledOrders)
73 .capability(Capability::FetchBalance)
75 .capability(Capability::FetchMyTrades)
76 .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 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 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 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 #[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 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 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 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 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 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 assert!(caps.websocket());
326 assert!(caps.watch_ticker());
327 assert!(caps.watch_order_book());
328 assert!(caps.watch_trades());
329
330 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 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 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 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 assert!(!caps.has("editOrder"));
420 assert!(!caps.has("cancelAllOrders"));
421 assert!(!caps.has("fetchCurrencies"));
422 assert!(!caps.has("unknownCapability"));
423 }
424}