ccxt_exchanges/okx/
rest.rs

1//! OKX REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the OKX exchange.
4
5use super::{Okx, OkxAuth, error, parser};
6use ccxt_core::{
7    Error, ParseError, Result,
8    types::{Balance, Market, OHLCV, Order, OrderBook, OrderSide, OrderType, Ticker, Trade},
9};
10use reqwest::header::{HeaderMap, HeaderValue};
11use serde_json::Value;
12use std::collections::HashMap;
13use tracing::{debug, info, warn};
14
15impl Okx {
16    // ============================================================================
17    // Helper Methods
18    // ============================================================================
19
20    /// Get the current timestamp in ISO 8601 format for OKX API.
21    fn get_timestamp(&self) -> String {
22        chrono::Utc::now()
23            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
24            .to_string()
25    }
26
27    /// Get the authentication instance if credentials are configured.
28    fn get_auth(&self) -> Result<OkxAuth> {
29        let config = &self.base().config;
30
31        let api_key = config
32            .api_key
33            .as_ref()
34            .ok_or_else(|| Error::authentication("API key is required"))?;
35        let secret = config
36            .secret
37            .as_ref()
38            .ok_or_else(|| Error::authentication("API secret is required"))?;
39        let passphrase = config
40            .password
41            .as_ref()
42            .ok_or_else(|| Error::authentication("Passphrase is required"))?;
43
44        Ok(OkxAuth::new(
45            api_key.clone(),
46            secret.clone(),
47            passphrase.clone(),
48        ))
49    }
50
51    /// Check that required credentials are configured.
52    pub fn check_required_credentials(&self) -> Result<()> {
53        self.base().check_required_credentials()?;
54        if self.base().config.password.is_none() {
55            return Err(Error::authentication("Passphrase is required for OKX"));
56        }
57        Ok(())
58    }
59
60    /// Build the API path for OKX V5 API.
61    fn build_api_path(&self, endpoint: &str) -> String {
62        format!("/api/v5{}", endpoint)
63    }
64
65    /// Get the instrument type for API requests.
66    fn get_inst_type(&self) -> &str {
67        match self.options().account_mode.as_str() {
68            "cross" | "isolated" => "MARGIN",
69            _ => "SPOT",
70        }
71    }
72
73    /// Make a public API request (no authentication required).
74    async fn public_request(
75        &self,
76        method: &str,
77        path: &str,
78        params: Option<&HashMap<String, String>>,
79    ) -> Result<Value> {
80        let urls = self.urls();
81        let mut url = format!("{}{}", urls.rest, path);
82
83        if let Some(p) = params {
84            if !p.is_empty() {
85                let query: Vec<String> = p
86                    .iter()
87                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
88                    .collect();
89                url = format!("{}?{}", url, query.join("&"));
90            }
91        }
92
93        debug!("OKX public request: {} {}", method, url);
94
95        // Add demo trading header if in sandbox mode
96        let mut headers = HeaderMap::new();
97        if self.options().demo || self.base().config.sandbox {
98            headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
99        }
100
101        let response = match method.to_uppercase().as_str() {
102            "GET" => {
103                if headers.is_empty() {
104                    self.base().http_client.get(&url, None).await?
105                } else {
106                    self.base().http_client.get(&url, Some(headers)).await?
107                }
108            }
109            "POST" => {
110                if headers.is_empty() {
111                    self.base().http_client.post(&url, None, None).await?
112                } else {
113                    self.base()
114                        .http_client
115                        .post(&url, Some(headers), None)
116                        .await?
117                }
118            }
119            _ => {
120                return Err(Error::invalid_request(format!(
121                    "Unsupported HTTP method: {}",
122                    method
123                )));
124            }
125        };
126
127        // Check for OKX error response
128        if error::is_error_response(&response) {
129            return Err(error::parse_error(&response));
130        }
131
132        Ok(response)
133    }
134
135    /// Make a private API request (authentication required).
136    async fn private_request(
137        &self,
138        method: &str,
139        path: &str,
140        params: Option<&HashMap<String, String>>,
141        body: Option<&Value>,
142    ) -> Result<Value> {
143        self.check_required_credentials()?;
144
145        let auth = self.get_auth()?;
146        let urls = self.urls();
147        let timestamp = self.get_timestamp();
148
149        // Build query string for GET requests
150        let query_string = if let Some(p) = params {
151            if !p.is_empty() {
152                let query: Vec<String> = p
153                    .iter()
154                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
155                    .collect();
156                format!("?{}", query.join("&"))
157            } else {
158                String::new()
159            }
160        } else {
161            String::new()
162        };
163
164        // Build body string for POST requests
165        let body_string = body
166            .map(|b| serde_json::to_string(b).unwrap_or_default())
167            .unwrap_or_default();
168
169        // Sign the request - OKX uses path with query string
170        let sign_path = format!("{}{}", path, query_string);
171        let signature = auth.sign(&timestamp, method, &sign_path, &body_string);
172
173        // Build headers
174        let mut headers = HeaderMap::new();
175        auth.add_auth_headers(&mut headers, &timestamp, &signature);
176        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
177
178        // Add demo trading header if in sandbox mode
179        if self.options().demo || self.base().config.sandbox {
180            headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
181        }
182
183        let url = format!("{}{}{}", urls.rest, path, query_string);
184        debug!("OKX private request: {} {}", method, url);
185
186        let response = match method.to_uppercase().as_str() {
187            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
188            "POST" => {
189                let body_value = body.cloned();
190                self.base()
191                    .http_client
192                    .post(&url, Some(headers), body_value)
193                    .await?
194            }
195            "DELETE" => {
196                self.base()
197                    .http_client
198                    .delete(&url, Some(headers), None)
199                    .await?
200            }
201            _ => {
202                return Err(Error::invalid_request(format!(
203                    "Unsupported HTTP method: {}",
204                    method
205                )));
206            }
207        };
208
209        // Check for OKX error response
210        if error::is_error_response(&response) {
211            return Err(error::parse_error(&response));
212        }
213
214        Ok(response)
215    }
216
217    // ============================================================================
218    // Public API Methods - Market Data
219    // ============================================================================
220
221    /// Fetch all trading markets.
222    ///
223    /// # Returns
224    ///
225    /// Returns a vector of [`Market`] structures containing market information.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if the API request fails or response parsing fails.
230    ///
231    /// # Example
232    ///
233    /// ```no_run
234    /// # use ccxt_exchanges::okx::Okx;
235    /// # async fn example() -> ccxt_core::Result<()> {
236    /// let okx = Okx::builder().build()?;
237    /// let markets = okx.fetch_markets().await?;
238    /// println!("Found {} markets", markets.len());
239    /// # Ok(())
240    /// # }
241    /// ```
242    pub async fn fetch_markets(&self) -> Result<Vec<Market>> {
243        let path = self.build_api_path("/public/instruments");
244        let mut params = HashMap::new();
245        params.insert("instType".to_string(), self.get_inst_type().to_string());
246
247        let response = self.public_request("GET", &path, Some(&params)).await?;
248
249        let data = response
250            .get("data")
251            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
252
253        let instruments = data.as_array().ok_or_else(|| {
254            Error::from(ParseError::invalid_format(
255                "data",
256                "Expected array of instruments",
257            ))
258        })?;
259
260        let mut markets = Vec::new();
261        for instrument in instruments {
262            match parser::parse_market(instrument) {
263                Ok(market) => markets.push(market),
264                Err(e) => {
265                    warn!(error = %e, "Failed to parse market");
266                }
267            }
268        }
269
270        // Cache the markets and preserve ownership for the caller
271        let markets = self.base().set_markets(markets, None).await?;
272
273        info!("Loaded {} markets for OKX", markets.len());
274        Ok(markets)
275    }
276
277    /// Load and cache market data.
278    ///
279    /// If markets are already loaded and `reload` is false, returns cached data.
280    ///
281    /// # Arguments
282    ///
283    /// * `reload` - Whether to force reload market data from the API.
284    ///
285    /// # Returns
286    ///
287    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
288    pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Market>> {
289        {
290            let cache = self.base().market_cache.read().await;
291            if cache.loaded && !reload {
292                debug!(
293                    "Returning cached markets for OKX ({} markets)",
294                    cache.markets.len()
295                );
296                return Ok(cache.markets.clone());
297            }
298        }
299
300        info!("Loading markets for OKX (reload: {})", reload);
301        let _markets = self.fetch_markets().await?;
302
303        let cache = self.base().market_cache.read().await;
304        Ok(cache.markets.clone())
305    }
306
307    /// Fetch ticker for a single trading pair.
308    ///
309    /// # Arguments
310    ///
311    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
312    ///
313    /// # Returns
314    ///
315    /// Returns [`Ticker`] data for the specified symbol.
316    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
317        let market = self.base().market(symbol).await?;
318
319        let path = self.build_api_path("/market/ticker");
320        let mut params = HashMap::new();
321        params.insert("instId".to_string(), market.id.clone());
322
323        let response = self.public_request("GET", &path, Some(&params)).await?;
324
325        let data = response
326            .get("data")
327            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
328
329        // OKX returns an array even for single ticker
330        let tickers = data.as_array().ok_or_else(|| {
331            Error::from(ParseError::invalid_format(
332                "data",
333                "Expected array of tickers",
334            ))
335        })?;
336
337        if tickers.is_empty() {
338            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
339        }
340
341        parser::parse_ticker(&tickers[0], Some(&market))
342    }
343
344    /// Fetch tickers for multiple trading pairs.
345    ///
346    /// # Arguments
347    ///
348    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
349    ///
350    /// # Returns
351    ///
352    /// Returns a vector of [`Ticker`] structures.
353    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
354        let cache = self.base().market_cache.read().await;
355        if !cache.loaded {
356            drop(cache);
357            return Err(Error::exchange(
358                "-1",
359                "Markets not loaded. Call load_markets() first.",
360            ));
361        }
362        drop(cache);
363
364        let path = self.build_api_path("/market/tickers");
365        let mut params = HashMap::new();
366        params.insert("instType".to_string(), self.get_inst_type().to_string());
367
368        let response = self.public_request("GET", &path, Some(&params)).await?;
369
370        let data = response
371            .get("data")
372            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
373
374        let tickers_array = data.as_array().ok_or_else(|| {
375            Error::from(ParseError::invalid_format(
376                "data",
377                "Expected array of tickers",
378            ))
379        })?;
380
381        let mut tickers = Vec::new();
382        for ticker_data in tickers_array {
383            if let Some(inst_id) = ticker_data["instId"].as_str() {
384                let cache = self.base().market_cache.read().await;
385                if let Some(market) = cache.markets_by_id.get(inst_id) {
386                    let market_clone = market.clone();
387                    drop(cache);
388
389                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
390                        Ok(ticker) => {
391                            if let Some(ref syms) = symbols {
392                                if syms.contains(&ticker.symbol) {
393                                    tickers.push(ticker);
394                                }
395                            } else {
396                                tickers.push(ticker);
397                            }
398                        }
399                        Err(e) => {
400                            warn!(
401                                error = %e,
402                                symbol = %inst_id,
403                                "Failed to parse ticker"
404                            );
405                        }
406                    }
407                } else {
408                    drop(cache);
409                }
410            }
411        }
412
413        Ok(tickers)
414    }
415
416    // ============================================================================
417    // Public API Methods - Order Book and Trades
418    // ============================================================================
419
420    /// Fetch order book for a trading pair.
421    ///
422    /// # Arguments
423    ///
424    /// * `symbol` - Trading pair symbol.
425    /// * `limit` - Optional depth limit (valid values: 1-400; default: 100).
426    ///
427    /// # Returns
428    ///
429    /// Returns [`OrderBook`] data containing bids and asks.
430    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
431        let market = self.base().market(symbol).await?;
432
433        let path = self.build_api_path("/market/books");
434        let mut params = HashMap::new();
435        params.insert("instId".to_string(), market.id.clone());
436
437        // OKX valid limits: 1-400, default 100
438        // Cap to maximum allowed value
439        let actual_limit = limit.map(|l| l.min(400)).unwrap_or(100);
440        params.insert("sz".to_string(), actual_limit.to_string());
441
442        let response = self.public_request("GET", &path, Some(&params)).await?;
443
444        let data = response
445            .get("data")
446            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
447
448        // OKX returns array with single orderbook
449        let books = data.as_array().ok_or_else(|| {
450            Error::from(ParseError::invalid_format(
451                "data",
452                "Expected array of orderbooks",
453            ))
454        })?;
455
456        if books.is_empty() {
457            return Err(Error::bad_symbol(format!(
458                "No orderbook data for {}",
459                symbol
460            )));
461        }
462
463        parser::parse_orderbook(&books[0], market.symbol.clone())
464    }
465
466    /// Fetch recent public trades.
467    ///
468    /// # Arguments
469    ///
470    /// * `symbol` - Trading pair symbol.
471    /// * `limit` - Optional limit on number of trades (maximum: 500).
472    ///
473    /// # Returns
474    ///
475    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
476    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
477        let market = self.base().market(symbol).await?;
478
479        let path = self.build_api_path("/market/trades");
480        let mut params = HashMap::new();
481        params.insert("instId".to_string(), market.id.clone());
482
483        // OKX maximum limit is 500
484        let actual_limit = limit.map(|l| l.min(500)).unwrap_or(100);
485        params.insert("limit".to_string(), actual_limit.to_string());
486
487        let response = self.public_request("GET", &path, Some(&params)).await?;
488
489        let data = response
490            .get("data")
491            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
492
493        let trades_array = data.as_array().ok_or_else(|| {
494            Error::from(ParseError::invalid_format(
495                "data",
496                "Expected array of trades",
497            ))
498        })?;
499
500        let mut trades = Vec::new();
501        for trade_data in trades_array {
502            match parser::parse_trade(trade_data, Some(&market)) {
503                Ok(trade) => trades.push(trade),
504                Err(e) => {
505                    warn!(error = %e, "Failed to parse trade");
506                }
507            }
508        }
509
510        // Sort by timestamp descending (newest first)
511        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
512
513        Ok(trades)
514    }
515
516    /// Fetch OHLCV (candlestick) data.
517    ///
518    /// # Arguments
519    ///
520    /// * `symbol` - Trading pair symbol.
521    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
522    /// * `since` - Optional start timestamp in milliseconds.
523    /// * `limit` - Optional limit on number of candles (maximum: 300).
524    ///
525    /// # Returns
526    ///
527    /// Returns a vector of [`OHLCV`] structures.
528    pub async fn fetch_ohlcv(
529        &self,
530        symbol: &str,
531        timeframe: &str,
532        since: Option<i64>,
533        limit: Option<u32>,
534    ) -> Result<Vec<OHLCV>> {
535        let market = self.base().market(symbol).await?;
536
537        // Convert timeframe to OKX format
538        let timeframes = self.timeframes();
539        let okx_timeframe = timeframes.get(timeframe).ok_or_else(|| {
540            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
541        })?;
542
543        let path = self.build_api_path("/market/candles");
544        let mut params = HashMap::new();
545        params.insert("instId".to_string(), market.id.clone());
546        params.insert("bar".to_string(), okx_timeframe.clone());
547
548        // OKX maximum limit is 300
549        let actual_limit = limit.map(|l| l.min(300)).unwrap_or(100);
550        params.insert("limit".to_string(), actual_limit.to_string());
551
552        if let Some(start_time) = since {
553            params.insert("after".to_string(), start_time.to_string());
554        }
555
556        let response = self.public_request("GET", &path, Some(&params)).await?;
557
558        let data = response
559            .get("data")
560            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
561
562        let candles_array = data.as_array().ok_or_else(|| {
563            Error::from(ParseError::invalid_format(
564                "data",
565                "Expected array of candles",
566            ))
567        })?;
568
569        let mut ohlcv = Vec::new();
570        for candle_data in candles_array {
571            match parser::parse_ohlcv(candle_data) {
572                Ok(candle) => ohlcv.push(candle),
573                Err(e) => {
574                    warn!(error = %e, "Failed to parse OHLCV");
575                }
576            }
577        }
578
579        // Sort by timestamp ascending (oldest first)
580        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
581
582        Ok(ohlcv)
583    }
584
585    // ============================================================================
586    // Private API Methods - Account
587    // ============================================================================
588
589    /// Fetch account balances.
590    ///
591    /// # Returns
592    ///
593    /// Returns a [`Balance`] structure with all currency balances.
594    ///
595    /// # Errors
596    ///
597    /// Returns an error if authentication fails or the API request fails.
598    pub async fn fetch_balance(&self) -> Result<Balance> {
599        let path = self.build_api_path("/account/balance");
600        let response = self.private_request("GET", &path, None, None).await?;
601
602        let data = response
603            .get("data")
604            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
605
606        // OKX returns array with single balance object
607        let balances = data.as_array().ok_or_else(|| {
608            Error::from(ParseError::invalid_format(
609                "data",
610                "Expected array of balances",
611            ))
612        })?;
613
614        if balances.is_empty() {
615            return Ok(Balance {
616                balances: HashMap::new(),
617                info: HashMap::new(),
618            });
619        }
620
621        parser::parse_balance(&balances[0])
622    }
623
624    /// Fetch user's trade history.
625    ///
626    /// # Arguments
627    ///
628    /// * `symbol` - Trading pair symbol.
629    /// * `since` - Optional start timestamp in milliseconds.
630    /// * `limit` - Optional limit on number of trades (maximum: 100).
631    ///
632    /// # Returns
633    ///
634    /// Returns a vector of [`Trade`] structures representing user's trade history.
635    pub async fn fetch_my_trades(
636        &self,
637        symbol: &str,
638        since: Option<i64>,
639        limit: Option<u32>,
640    ) -> Result<Vec<Trade>> {
641        let market = self.base().market(symbol).await?;
642
643        let path = self.build_api_path("/trade/fills");
644        let mut params = HashMap::new();
645        params.insert("instId".to_string(), market.id.clone());
646        params.insert("instType".to_string(), self.get_inst_type().to_string());
647
648        // OKX maximum limit is 100
649        let actual_limit = limit.map(|l| l.min(100)).unwrap_or(100);
650        params.insert("limit".to_string(), actual_limit.to_string());
651
652        if let Some(start_time) = since {
653            params.insert("begin".to_string(), start_time.to_string());
654        }
655
656        let response = self
657            .private_request("GET", &path, Some(&params), None)
658            .await?;
659
660        let data = response
661            .get("data")
662            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
663
664        let trades_array = data.as_array().ok_or_else(|| {
665            Error::from(ParseError::invalid_format(
666                "data",
667                "Expected array of trades",
668            ))
669        })?;
670
671        let mut trades = Vec::new();
672        for trade_data in trades_array {
673            match parser::parse_trade(trade_data, Some(&market)) {
674                Ok(trade) => trades.push(trade),
675                Err(e) => {
676                    warn!(error = %e, "Failed to parse my trade");
677                }
678            }
679        }
680
681        Ok(trades)
682    }
683
684    // ============================================================================
685    // Private API Methods - Order Management
686    // ============================================================================
687
688    /// Create a new order.
689    ///
690    /// # Arguments
691    ///
692    /// * `symbol` - Trading pair symbol.
693    /// * `order_type` - Order type (Market, Limit).
694    /// * `side` - Order side (Buy or Sell).
695    /// * `amount` - Order quantity.
696    /// * `price` - Optional price (required for limit orders).
697    ///
698    /// # Returns
699    ///
700    /// Returns the created [`Order`] structure with order details.
701    pub async fn create_order(
702        &self,
703        symbol: &str,
704        order_type: OrderType,
705        side: OrderSide,
706        amount: f64,
707        price: Option<f64>,
708    ) -> Result<Order> {
709        let market = self.base().market(symbol).await?;
710
711        let path = self.build_api_path("/trade/order");
712
713        // Build order body
714        let mut map = serde_json::Map::new();
715        map.insert(
716            "instId".to_string(),
717            serde_json::Value::String(market.id.clone()),
718        );
719        map.insert(
720            "tdMode".to_string(),
721            serde_json::Value::String(self.options().account_mode.clone()),
722        );
723        map.insert(
724            "side".to_string(),
725            serde_json::Value::String(match side {
726                OrderSide::Buy => "buy".to_string(),
727                OrderSide::Sell => "sell".to_string(),
728            }),
729        );
730        map.insert(
731            "ordType".to_string(),
732            serde_json::Value::String(match order_type {
733                OrderType::Market => "market".to_string(),
734                OrderType::Limit => "limit".to_string(),
735                OrderType::LimitMaker => "post_only".to_string(),
736                _ => "limit".to_string(),
737            }),
738        );
739        map.insert(
740            "sz".to_string(),
741            serde_json::Value::String(amount.to_string()),
742        );
743
744        // Add price for limit orders
745        if let Some(p) = price {
746            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
747                map.insert("px".to_string(), serde_json::Value::String(p.to_string()));
748            }
749        }
750        let body = serde_json::Value::Object(map);
751
752        let response = self
753            .private_request("POST", &path, None, Some(&body))
754            .await?;
755
756        let data = response
757            .get("data")
758            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
759
760        // OKX returns array with single order
761        let orders = data.as_array().ok_or_else(|| {
762            Error::from(ParseError::invalid_format(
763                "data",
764                "Expected array of orders",
765            ))
766        })?;
767
768        if orders.is_empty() {
769            return Err(Error::exchange("-1", "No order data returned"));
770        }
771
772        parser::parse_order(&orders[0], Some(&market))
773    }
774
775    /// Cancel an existing order.
776    ///
777    /// # Arguments
778    ///
779    /// * `id` - Order ID to cancel.
780    /// * `symbol` - Trading pair symbol.
781    ///
782    /// # Returns
783    ///
784    /// Returns the canceled [`Order`] structure.
785    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
786        let market = self.base().market(symbol).await?;
787
788        let path = self.build_api_path("/trade/cancel-order");
789
790        let mut map = serde_json::Map::new();
791        map.insert(
792            "instId".to_string(),
793            serde_json::Value::String(market.id.clone()),
794        );
795        map.insert(
796            "ordId".to_string(),
797            serde_json::Value::String(id.to_string()),
798        );
799        let body = serde_json::Value::Object(map);
800
801        let response = self
802            .private_request("POST", &path, None, Some(&body))
803            .await?;
804
805        let data = response
806            .get("data")
807            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
808
809        // OKX returns array with single order
810        let orders = data.as_array().ok_or_else(|| {
811            Error::from(ParseError::invalid_format(
812                "data",
813                "Expected array of orders",
814            ))
815        })?;
816
817        if orders.is_empty() {
818            return Err(Error::exchange("-1", "No order data returned"));
819        }
820
821        parser::parse_order(&orders[0], Some(&market))
822    }
823
824    /// Fetch a single order by ID.
825    ///
826    /// # Arguments
827    ///
828    /// * `id` - Order ID to fetch.
829    /// * `symbol` - Trading pair symbol.
830    ///
831    /// # Returns
832    ///
833    /// Returns the [`Order`] structure with current status.
834    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
835        let market = self.base().market(symbol).await?;
836
837        let path = self.build_api_path("/trade/order");
838        let mut params = HashMap::new();
839        params.insert("instId".to_string(), market.id.clone());
840        params.insert("ordId".to_string(), id.to_string());
841
842        let response = self
843            .private_request("GET", &path, Some(&params), None)
844            .await?;
845
846        let data = response
847            .get("data")
848            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
849
850        // OKX returns array with single order
851        let orders = data.as_array().ok_or_else(|| {
852            Error::from(ParseError::invalid_format(
853                "data",
854                "Expected array of orders",
855            ))
856        })?;
857
858        if orders.is_empty() {
859            return Err(Error::exchange("51400", "Order not found"));
860        }
861
862        parser::parse_order(&orders[0], Some(&market))
863    }
864
865    /// Fetch open orders.
866    ///
867    /// # Arguments
868    ///
869    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
870    /// * `since` - Optional start timestamp in milliseconds.
871    /// * `limit` - Optional limit on number of orders (maximum: 100).
872    ///
873    /// # Returns
874    ///
875    /// Returns a vector of open [`Order`] structures.
876    pub async fn fetch_open_orders(
877        &self,
878        symbol: Option<&str>,
879        since: Option<i64>,
880        limit: Option<u32>,
881    ) -> Result<Vec<Order>> {
882        let path = self.build_api_path("/trade/orders-pending");
883        let mut params = HashMap::new();
884        params.insert("instType".to_string(), self.get_inst_type().to_string());
885
886        let market = if let Some(sym) = symbol {
887            let m = self.base().market(sym).await?;
888            params.insert("instId".to_string(), m.id.clone());
889            Some(m)
890        } else {
891            None
892        };
893
894        // OKX maximum limit is 100
895        let actual_limit = limit.map(|l| l.min(100)).unwrap_or(100);
896        params.insert("limit".to_string(), actual_limit.to_string());
897
898        if let Some(start_time) = since {
899            params.insert("begin".to_string(), start_time.to_string());
900        }
901
902        let response = self
903            .private_request("GET", &path, Some(&params), None)
904            .await?;
905
906        let data = response
907            .get("data")
908            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
909
910        let orders_array = data.as_array().ok_or_else(|| {
911            Error::from(ParseError::invalid_format(
912                "data",
913                "Expected array of orders",
914            ))
915        })?;
916
917        let mut orders = Vec::new();
918        for order_data in orders_array {
919            match parser::parse_order(order_data, market.as_ref()) {
920                Ok(order) => orders.push(order),
921                Err(e) => {
922                    warn!(error = %e, "Failed to parse open order");
923                }
924            }
925        }
926
927        Ok(orders)
928    }
929
930    /// Fetch closed orders.
931    ///
932    /// # Arguments
933    ///
934    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
935    /// * `since` - Optional start timestamp in milliseconds.
936    /// * `limit` - Optional limit on number of orders (maximum: 100).
937    ///
938    /// # Returns
939    ///
940    /// Returns a vector of closed [`Order`] structures.
941    pub async fn fetch_closed_orders(
942        &self,
943        symbol: Option<&str>,
944        since: Option<i64>,
945        limit: Option<u32>,
946    ) -> Result<Vec<Order>> {
947        let path = self.build_api_path("/trade/orders-history");
948        let mut params = HashMap::new();
949        params.insert("instType".to_string(), self.get_inst_type().to_string());
950
951        let market = if let Some(sym) = symbol {
952            let m = self.base().market(sym).await?;
953            params.insert("instId".to_string(), m.id.clone());
954            Some(m)
955        } else {
956            None
957        };
958
959        // OKX maximum limit is 100
960        let actual_limit = limit.map(|l| l.min(100)).unwrap_or(100);
961        params.insert("limit".to_string(), actual_limit.to_string());
962
963        if let Some(start_time) = since {
964            params.insert("begin".to_string(), start_time.to_string());
965        }
966
967        let response = self
968            .private_request("GET", &path, Some(&params), None)
969            .await?;
970
971        let data = response
972            .get("data")
973            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
974
975        let orders_array = data.as_array().ok_or_else(|| {
976            Error::from(ParseError::invalid_format(
977                "data",
978                "Expected array of orders",
979            ))
980        })?;
981
982        let mut orders = Vec::new();
983        for order_data in orders_array {
984            match parser::parse_order(order_data, market.as_ref()) {
985                Ok(order) => orders.push(order),
986                Err(e) => {
987                    warn!(error = %e, "Failed to parse closed order");
988                }
989            }
990        }
991
992        Ok(orders)
993    }
994}
995
996#[cfg(test)]
997mod tests {
998    use super::*;
999
1000    #[test]
1001    fn test_build_api_path() {
1002        let okx = Okx::builder().build().unwrap();
1003        let path = okx.build_api_path("/public/instruments");
1004        assert_eq!(path, "/api/v5/public/instruments");
1005    }
1006
1007    #[test]
1008    fn test_get_inst_type_spot() {
1009        let okx = Okx::builder().build().unwrap();
1010        let inst_type = okx.get_inst_type();
1011        assert_eq!(inst_type, "SPOT");
1012    }
1013
1014    #[test]
1015    fn test_get_inst_type_margin() {
1016        let okx = Okx::builder().account_mode("cross").build().unwrap();
1017        let inst_type = okx.get_inst_type();
1018        assert_eq!(inst_type, "MARGIN");
1019    }
1020
1021    #[test]
1022    fn test_get_timestamp() {
1023        let okx = Okx::builder().build().unwrap();
1024        let ts = okx.get_timestamp();
1025
1026        // Should be in ISO 8601 format
1027        assert!(ts.contains("T"));
1028        assert!(ts.contains("Z"));
1029        assert!(ts.len() > 20);
1030    }
1031}