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