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 crate::okx::signed_request::HttpMethod;
7use ccxt_core::{
8    Error, ParseError, Result,
9    types::{
10        Amount, Balance, Market, OHLCV, OhlcvRequest, Order, OrderBook, OrderRequest, OrderSide,
11        OrderType, Price, Ticker, TimeInForce, Trade,
12    },
13};
14use reqwest::header::{HeaderMap, HeaderValue};
15use serde_json::Value;
16use std::{collections::HashMap, sync::Arc};
17use tracing::{debug, info, warn};
18
19impl Okx {
20    // ============================================================================
21    // Helper Methods
22    // ============================================================================
23
24    /// Get the current timestamp in ISO 8601 format for OKX API.
25    ///
26    /// # Deprecated
27    ///
28    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
29    /// The `signed_request()` builder handles timestamp generation internally.
30    #[deprecated(
31        since = "0.1.0",
32        note = "Use `signed_request()` builder instead which handles timestamps internally"
33    )]
34    #[allow(dead_code)]
35    fn get_timestamp() -> String {
36        chrono::Utc::now()
37            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
38            .to_string()
39    }
40
41    /// Get the authentication instance if credentials are configured.
42    pub fn get_auth(&self) -> Result<OkxAuth> {
43        let config = &self.base().config;
44
45        let api_key = config
46            .api_key
47            .as_ref()
48            .ok_or_else(|| Error::authentication("API key is required"))?;
49        let secret = config
50            .secret
51            .as_ref()
52            .ok_or_else(|| Error::authentication("API secret is required"))?;
53        let passphrase = config
54            .password
55            .as_ref()
56            .ok_or_else(|| Error::authentication("Passphrase is required"))?;
57
58        Ok(OkxAuth::new(
59            api_key.expose_secret().to_string(),
60            secret.expose_secret().to_string(),
61            passphrase.expose_secret().to_string(),
62        ))
63    }
64
65    /// Check that required credentials are configured.
66    pub fn check_required_credentials(&self) -> Result<()> {
67        self.base().check_required_credentials()?;
68        if self.base().config.password.is_none() {
69            return Err(Error::authentication("Passphrase is required for OKX"));
70        }
71        Ok(())
72    }
73
74    /// Build the API path for OKX V5 API.
75    fn build_api_path(endpoint: &str) -> String {
76        format!("/api/v5{}", endpoint)
77    }
78
79    /// Get the instrument type for API requests.
80    ///
81    /// Maps the configured `default_type` to OKX's instrument type (instType) parameter.
82    /// OKX uses a unified V5 API, so this primarily affects market filtering.
83    ///
84    /// # Returns
85    ///
86    /// Returns the OKX instrument type string:
87    /// - "SPOT" for spot trading
88    /// - "MARGIN" for margin trading
89    /// - "SWAP" for perpetual contracts
90    /// - "FUTURES" for delivery contracts
91    /// - "OPTION" for options trading
92    pub fn get_inst_type(&self) -> &str {
93        use ccxt_core::types::default_type::DefaultType;
94
95        match self.options().default_type {
96            DefaultType::Spot => "SPOT",
97            DefaultType::Margin => "MARGIN",
98            DefaultType::Swap => "SWAP",
99            DefaultType::Futures => "FUTURES",
100            DefaultType::Option => "OPTION",
101        }
102    }
103
104    /// Make a public API request (no authentication required).
105    async fn public_request(
106        &self,
107        method: &str,
108        path: &str,
109        params: Option<&HashMap<String, String>>,
110    ) -> Result<Value> {
111        let urls = self.urls();
112        let mut url = format!("{}{}", urls.rest, path);
113
114        if let Some(p) = params {
115            if !p.is_empty() {
116                let query: Vec<String> = p
117                    .iter()
118                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
119                    .collect();
120                url = format!("{}?{}", url, query.join("&"));
121            }
122        }
123
124        debug!("OKX public request: {} {}", method, url);
125
126        // Add demo trading header if in sandbox mode
127        let mut headers = HeaderMap::new();
128        if self.is_testnet_trading() {
129            headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
130        }
131
132        let response = match method.to_uppercase().as_str() {
133            "GET" => {
134                if headers.is_empty() {
135                    self.base().http_client.get(&url, None).await?
136                } else {
137                    self.base().http_client.get(&url, Some(headers)).await?
138                }
139            }
140            "POST" => {
141                if headers.is_empty() {
142                    self.base().http_client.post(&url, None, None).await?
143                } else {
144                    self.base()
145                        .http_client
146                        .post(&url, Some(headers), None)
147                        .await?
148                }
149            }
150            _ => {
151                return Err(Error::invalid_request(format!(
152                    "Unsupported HTTP method: {}",
153                    method
154                )));
155            }
156        };
157
158        // Check for OKX error response
159        if error::is_error_response(&response) {
160            return Err(error::parse_error(&response));
161        }
162
163        Ok(response)
164    }
165
166    /// Make a private API request (authentication required).
167    ///
168    /// # Deprecated
169    ///
170    /// This method is deprecated. Use [`signed_request()`](Self::signed_request) instead.
171    /// The `signed_request()` builder provides a cleaner, more maintainable API for
172    /// constructing authenticated requests.
173    #[deprecated(
174        since = "0.1.0",
175        note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
176    )]
177    #[allow(dead_code)]
178    #[allow(deprecated)]
179    async fn private_request(
180        &self,
181        method: &str,
182        path: &str,
183        params: Option<&HashMap<String, String>>,
184        body: Option<&Value>,
185    ) -> Result<Value> {
186        self.check_required_credentials()?;
187
188        let auth = self.get_auth()?;
189        let urls = self.urls();
190        let timestamp = Self::get_timestamp();
191
192        // Build query string for GET requests
193        let query_string = if let Some(p) = params {
194            if p.is_empty() {
195                String::new()
196            } else {
197                let query: Vec<String> = p
198                    .iter()
199                    .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
200                    .collect();
201                format!("?{}", query.join("&"))
202            }
203        } else {
204            String::new()
205        };
206
207        // Build body string for POST requests
208        let body_string = body
209            .map(|b| serde_json::to_string(b).unwrap_or_default())
210            .unwrap_or_default();
211
212        // Sign the request - OKX uses path with query string
213        let sign_path = format!("{}{}", path, query_string);
214        let signature = auth.sign(&timestamp, method, &sign_path, &body_string);
215
216        // Build headers
217        let mut headers = HeaderMap::new();
218        auth.add_auth_headers(&mut headers, &timestamp, &signature);
219        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
220
221        // Add demo trading header if in sandbox mode
222        if self.is_testnet_trading() {
223            headers.insert("x-simulated-trading", HeaderValue::from_static("1"));
224        }
225
226        let url = format!("{}{}{}", urls.rest, path, query_string);
227        debug!("OKX private request: {} {}", method, url);
228
229        let response = match method.to_uppercase().as_str() {
230            "GET" => self.base().http_client.get(&url, Some(headers)).await?,
231            "POST" => {
232                let body_value = body.cloned();
233                self.base()
234                    .http_client
235                    .post(&url, Some(headers), body_value)
236                    .await?
237            }
238            "DELETE" => {
239                self.base()
240                    .http_client
241                    .delete(&url, Some(headers), None)
242                    .await?
243            }
244            _ => {
245                return Err(Error::invalid_request(format!(
246                    "Unsupported HTTP method: {}",
247                    method
248                )));
249            }
250        };
251
252        // Check for OKX error response
253        if error::is_error_response(&response) {
254            return Err(error::parse_error(&response));
255        }
256
257        Ok(response)
258    }
259
260    // ============================================================================
261    // Public API Methods - Market Data
262    // ============================================================================
263
264    /// Fetch all trading markets.
265    ///
266    /// # Returns
267    ///
268    /// Returns a vector of [`Market`] structures containing market information.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if the API request fails or response parsing fails.
273    ///
274    /// # Example
275    ///
276    /// ```no_run
277    /// # use ccxt_exchanges::okx::Okx;
278    /// # async fn example() -> ccxt_core::Result<()> {
279    /// let okx = Okx::builder().build()?;
280    /// let markets = okx.fetch_markets().await?;
281    /// println!("Found {} markets", markets.len());
282    /// # Ok(())
283    /// # }
284    /// ```
285    pub async fn fetch_markets(&self) -> Result<Arc<HashMap<String, Arc<Market>>>> {
286        let path = Self::build_api_path("/public/instruments");
287        let mut params = HashMap::new();
288        params.insert("instType".to_string(), self.get_inst_type().to_string());
289
290        let response = self.public_request("GET", &path, Some(&params)).await?;
291
292        let data = response
293            .get("data")
294            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
295
296        let instruments = data.as_array().ok_or_else(|| {
297            Error::from(ParseError::invalid_format(
298                "data",
299                "Expected array of instruments",
300            ))
301        })?;
302
303        let mut markets = Vec::new();
304        for instrument in instruments {
305            match parser::parse_market(instrument) {
306                Ok(market) => markets.push(market),
307                Err(e) => {
308                    warn!(error = %e, "Failed to parse market");
309                }
310            }
311        }
312
313        // Cache the markets and preserve ownership for the caller
314        let result = self.base().set_markets(markets, None).await?;
315
316        info!("Loaded {} markets for OKX", result.len());
317        Ok(result)
318    }
319
320    /// Load and cache market data.
321    ///
322    /// If markets are already loaded and `reload` is false, returns cached data.
323    ///
324    /// # Arguments
325    ///
326    /// * `reload` - Whether to force reload market data from the API.
327    ///
328    /// # Returns
329    ///
330    /// Returns a `HashMap` containing all market data, keyed by symbol (e.g., "BTC/USDT").
331    pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
332        // Acquire the loading lock to serialize concurrent load_markets calls
333        // This prevents multiple tasks from making duplicate API calls
334        let _loading_guard = self.base().market_loading_lock.lock().await;
335
336        // Check cache status while holding the lock
337        {
338            let cache = self.base().market_cache.read().await;
339            if cache.loaded && !reload {
340                debug!(
341                    "Returning cached markets for OKX ({} markets)",
342                    cache.markets.len()
343                );
344                return Ok(cache.markets.clone());
345            }
346        }
347
348        info!("Loading markets for OKX (reload: {})", reload);
349        let _markets = self.fetch_markets().await?;
350
351        let cache = self.base().market_cache.read().await;
352        Ok(cache.markets.clone())
353    }
354
355    /// Fetch ticker for a single trading pair.
356    ///
357    /// # Arguments
358    ///
359    /// * `symbol` - Trading pair symbol (e.g., "BTC/USDT").
360    ///
361    /// # Returns
362    ///
363    /// Returns [`Ticker`] data for the specified symbol.
364    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
365        let market = self.base().market(symbol).await?;
366
367        let path = Self::build_api_path("/market/ticker");
368        let mut params = HashMap::new();
369        params.insert("instId".to_string(), market.id.clone());
370
371        let response = self.public_request("GET", &path, Some(&params)).await?;
372
373        let data = response
374            .get("data")
375            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
376
377        // OKX returns an array even for single ticker
378        let tickers = data.as_array().ok_or_else(|| {
379            Error::from(ParseError::invalid_format(
380                "data",
381                "Expected array of tickers",
382            ))
383        })?;
384
385        if tickers.is_empty() {
386            return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
387        }
388
389        parser::parse_ticker(&tickers[0], Some(&market))
390    }
391
392    /// Fetch tickers for multiple trading pairs.
393    ///
394    /// # Arguments
395    ///
396    /// * `symbols` - Optional list of trading pair symbols; fetches all if `None`.
397    ///
398    /// # Returns
399    ///
400    /// Returns a vector of [`Ticker`] structures.
401    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
402        let cache = self.base().market_cache.read().await;
403        if !cache.loaded {
404            drop(cache);
405            return Err(Error::exchange(
406                "-1",
407                "Markets not loaded. Call load_markets() first.",
408            ));
409        }
410        drop(cache);
411
412        let path = Self::build_api_path("/market/tickers");
413        let mut params = HashMap::new();
414        params.insert("instType".to_string(), self.get_inst_type().to_string());
415
416        let response = self.public_request("GET", &path, Some(&params)).await?;
417
418        let data = response
419            .get("data")
420            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
421
422        let tickers_array = data.as_array().ok_or_else(|| {
423            Error::from(ParseError::invalid_format(
424                "data",
425                "Expected array of tickers",
426            ))
427        })?;
428
429        let mut tickers = Vec::new();
430        for ticker_data in tickers_array {
431            if let Some(inst_id) = ticker_data["instId"].as_str() {
432                let cache = self.base().market_cache.read().await;
433                if let Some(market) = cache.markets_by_id.get(inst_id) {
434                    let market_clone = market.clone();
435                    drop(cache);
436
437                    match parser::parse_ticker(ticker_data, Some(&market_clone)) {
438                        Ok(ticker) => {
439                            if let Some(ref syms) = symbols {
440                                if syms.contains(&ticker.symbol) {
441                                    tickers.push(ticker);
442                                }
443                            } else {
444                                tickers.push(ticker);
445                            }
446                        }
447                        Err(e) => {
448                            warn!(
449                                error = %e,
450                                symbol = %inst_id,
451                                "Failed to parse ticker"
452                            );
453                        }
454                    }
455                } else {
456                    drop(cache);
457                }
458            }
459        }
460
461        Ok(tickers)
462    }
463
464    // ============================================================================
465    // Public API Methods - Order Book and Trades
466    // ============================================================================
467
468    /// Fetch order book for a trading pair.
469    ///
470    /// # Arguments
471    ///
472    /// * `symbol` - Trading pair symbol.
473    /// * `limit` - Optional depth limit (valid values: 1-400; default: 100).
474    ///
475    /// # Returns
476    ///
477    /// Returns [`OrderBook`] data containing bids and asks.
478    pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
479        let market = self.base().market(symbol).await?;
480
481        let path = Self::build_api_path("/market/books");
482        let mut params = HashMap::new();
483        params.insert("instId".to_string(), market.id.clone());
484
485        // OKX valid limits: 1-400, default 100
486        // Cap to maximum allowed value
487        let actual_limit = limit.map_or(100, |l| l.min(400));
488        params.insert("sz".to_string(), actual_limit.to_string());
489
490        let response = self.public_request("GET", &path, Some(&params)).await?;
491
492        let data = response
493            .get("data")
494            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
495
496        // OKX returns array with single orderbook
497        let books = data.as_array().ok_or_else(|| {
498            Error::from(ParseError::invalid_format(
499                "data",
500                "Expected array of orderbooks",
501            ))
502        })?;
503
504        if books.is_empty() {
505            return Err(Error::bad_symbol(format!(
506                "No orderbook data for {}",
507                symbol
508            )));
509        }
510
511        parser::parse_orderbook(&books[0], market.symbol.clone())
512    }
513
514    /// Fetch recent public trades.
515    ///
516    /// # Arguments
517    ///
518    /// * `symbol` - Trading pair symbol.
519    /// * `limit` - Optional limit on number of trades (maximum: 500).
520    ///
521    /// # Returns
522    ///
523    /// Returns a vector of [`Trade`] structures, sorted by timestamp in descending order.
524    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
525        let market = self.base().market(symbol).await?;
526
527        let path = Self::build_api_path("/market/trades");
528        let mut params = HashMap::new();
529        params.insert("instId".to_string(), market.id.clone());
530
531        // OKX maximum limit is 500
532        let actual_limit = limit.map_or(100, |l| l.min(500));
533        params.insert("limit".to_string(), actual_limit.to_string());
534
535        let response = self.public_request("GET", &path, Some(&params)).await?;
536
537        let data = response
538            .get("data")
539            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
540
541        let trades_array = data.as_array().ok_or_else(|| {
542            Error::from(ParseError::invalid_format(
543                "data",
544                "Expected array of trades",
545            ))
546        })?;
547
548        let mut trades = Vec::new();
549        for trade_data in trades_array {
550            match parser::parse_trade(trade_data, Some(&market)) {
551                Ok(trade) => trades.push(trade),
552                Err(e) => {
553                    warn!(error = %e, "Failed to parse trade");
554                }
555            }
556        }
557
558        // Sort by timestamp descending (newest first)
559        trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
560
561        Ok(trades)
562    }
563
564    /// Fetch OHLCV (candlestick) data using the builder pattern.
565    ///
566    /// This is the preferred method for fetching OHLCV data. It accepts an [`OhlcvRequest`]
567    /// built using the builder pattern, which provides validation and a more ergonomic API.
568    ///
569    /// # Arguments
570    ///
571    /// * `request` - OHLCV request built via [`OhlcvRequest::builder()`]
572    ///
573    /// # Returns
574    ///
575    /// Returns a vector of [`OHLCV`] structures.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if the market is not found or the API request fails.
580    ///
581    /// _Requirements: 2.3, 2.6_
582    pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
583        let market = self.base().market(&request.symbol).await?;
584
585        // Convert timeframe to OKX format
586        let timeframes = self.timeframes();
587        let okx_timeframe = timeframes.get(&request.timeframe).ok_or_else(|| {
588            Error::invalid_request(format!("Unsupported timeframe: {}", request.timeframe))
589        })?;
590
591        let path = Self::build_api_path("/market/candles");
592        let mut params = HashMap::new();
593        params.insert("instId".to_string(), market.id.clone());
594        params.insert("bar".to_string(), okx_timeframe.clone());
595
596        // OKX maximum limit is 300
597        let actual_limit = request.limit.map_or(100, |l| l.min(300));
598        params.insert("limit".to_string(), actual_limit.to_string());
599
600        if let Some(start_time) = request.since {
601            params.insert("after".to_string(), start_time.to_string());
602        }
603
604        if let Some(end_time) = request.until {
605            params.insert("before".to_string(), end_time.to_string());
606        }
607
608        let response = self.public_request("GET", &path, Some(&params)).await?;
609
610        let data = response
611            .get("data")
612            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
613
614        let candles_array = data.as_array().ok_or_else(|| {
615            Error::from(ParseError::invalid_format(
616                "data",
617                "Expected array of candles",
618            ))
619        })?;
620
621        let mut ohlcv = Vec::new();
622        for candle_data in candles_array {
623            match parser::parse_ohlcv(candle_data) {
624                Ok(candle) => ohlcv.push(candle),
625                Err(e) => {
626                    warn!(error = %e, "Failed to parse OHLCV");
627                }
628            }
629        }
630
631        // Sort by timestamp ascending (oldest first)
632        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
633
634        Ok(ohlcv)
635    }
636
637    /// Fetch OHLCV (candlestick) data (deprecated).
638    ///
639    /// # Deprecated
640    ///
641    /// This method is deprecated. Use [`fetch_ohlcv_v2`](Self::fetch_ohlcv_v2) with
642    /// [`OhlcvRequest::builder()`] instead for a more ergonomic API.
643    ///
644    /// # Arguments
645    ///
646    /// * `symbol` - Trading pair symbol.
647    /// * `timeframe` - Candlestick timeframe (e.g., "1m", "5m", "1h", "1d").
648    /// * `since` - Optional start timestamp in milliseconds.
649    /// * `limit` - Optional limit on number of candles (maximum: 300).
650    ///
651    /// # Returns
652    ///
653    /// Returns a vector of [`OHLCV`] structures.
654    #[deprecated(
655        since = "0.2.0",
656        note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
657    )]
658    pub async fn fetch_ohlcv(
659        &self,
660        symbol: &str,
661        timeframe: &str,
662        since: Option<i64>,
663        limit: Option<u32>,
664    ) -> Result<Vec<OHLCV>> {
665        let market = self.base().market(symbol).await?;
666
667        // Convert timeframe to OKX format
668        let timeframes = self.timeframes();
669        let okx_timeframe = timeframes.get(timeframe).ok_or_else(|| {
670            Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
671        })?;
672
673        let path = Self::build_api_path("/market/candles");
674        let mut params = HashMap::new();
675        params.insert("instId".to_string(), market.id.clone());
676        params.insert("bar".to_string(), okx_timeframe.clone());
677
678        // OKX maximum limit is 300
679        let actual_limit = limit.map_or(100, |l| l.min(300));
680        params.insert("limit".to_string(), actual_limit.to_string());
681
682        if let Some(start_time) = since {
683            params.insert("after".to_string(), start_time.to_string());
684        }
685
686        let response = self.public_request("GET", &path, Some(&params)).await?;
687
688        let data = response
689            .get("data")
690            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
691
692        let candles_array = data.as_array().ok_or_else(|| {
693            Error::from(ParseError::invalid_format(
694                "data",
695                "Expected array of candles",
696            ))
697        })?;
698
699        let mut ohlcv = Vec::new();
700        for candle_data in candles_array {
701            match parser::parse_ohlcv(candle_data) {
702                Ok(candle) => ohlcv.push(candle),
703                Err(e) => {
704                    warn!(error = %e, "Failed to parse OHLCV");
705                }
706            }
707        }
708
709        // Sort by timestamp ascending (oldest first)
710        ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
711
712        Ok(ohlcv)
713    }
714
715    // ============================================================================
716    // Private API Methods - Account
717    // ============================================================================
718
719    /// Fetch account balances.
720    ///
721    /// # Returns
722    ///
723    /// Returns a [`Balance`] structure with all currency balances.
724    ///
725    /// # Errors
726    ///
727    /// Returns an error if authentication fails or the API request fails.
728    pub async fn fetch_balance(&self) -> Result<Balance> {
729        let path = Self::build_api_path("/account/balance");
730        let response = self.signed_request(&path).execute().await?;
731
732        let data = response
733            .get("data")
734            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
735
736        // OKX returns array with single balance object
737        let balances = data.as_array().ok_or_else(|| {
738            Error::from(ParseError::invalid_format(
739                "data",
740                "Expected array of balances",
741            ))
742        })?;
743
744        if balances.is_empty() {
745            return Ok(Balance {
746                balances: HashMap::new(),
747                info: HashMap::new(),
748            });
749        }
750
751        parser::parse_balance(&balances[0])
752    }
753
754    /// Fetch user's trade history.
755    ///
756    /// # Arguments
757    ///
758    /// * `symbol` - Trading pair symbol.
759    /// * `since` - Optional start timestamp in milliseconds.
760    /// * `limit` - Optional limit on number of trades (maximum: 100).
761    ///
762    /// # Returns
763    ///
764    /// Returns a vector of [`Trade`] structures representing user's trade history.
765    pub async fn fetch_my_trades(
766        &self,
767        symbol: &str,
768        since: Option<i64>,
769        limit: Option<u32>,
770    ) -> Result<Vec<Trade>> {
771        let market = self.base().market(symbol).await?;
772
773        let path = Self::build_api_path("/trade/fills");
774
775        // OKX maximum limit is 100
776        let actual_limit = limit.map_or(100, |l| l.min(100));
777
778        let mut builder = self
779            .signed_request(&path)
780            .param("instId", &market.id)
781            .param("instType", self.get_inst_type())
782            .param("limit", actual_limit);
783
784        if let Some(start_time) = since {
785            builder = builder.param("begin", start_time);
786        }
787
788        let response = builder.execute().await?;
789
790        let data = response
791            .get("data")
792            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
793
794        let trades_array = data.as_array().ok_or_else(|| {
795            Error::from(ParseError::invalid_format(
796                "data",
797                "Expected array of trades",
798            ))
799        })?;
800
801        let mut trades = Vec::new();
802        for trade_data in trades_array {
803            match parser::parse_trade(trade_data, Some(&market)) {
804                Ok(trade) => trades.push(trade),
805                Err(e) => {
806                    warn!(error = %e, "Failed to parse my trade");
807                }
808            }
809        }
810
811        Ok(trades)
812    }
813
814    // ============================================================================
815    // Private API Methods - Order Management
816    // ============================================================================
817
818    /// Create a new order using the builder pattern.
819    ///
820    /// This is the preferred method for creating orders. It accepts an [`OrderRequest`]
821    /// built using the builder pattern, which provides compile-time validation of
822    /// required fields and a more ergonomic API.
823    ///
824    /// # Arguments
825    ///
826    /// * `request` - Order request built via [`OrderRequest::builder()`]
827    ///
828    /// # Returns
829    ///
830    /// Returns the created [`Order`] structure with order details.
831    ///
832    /// # Errors
833    ///
834    /// Returns an error if authentication fails, market is not found, or the API request fails.
835    ///
836    /// _Requirements: 2.2, 2.6_
837    pub async fn create_order_v2(&self, request: OrderRequest) -> Result<Order> {
838        let market = self.base().market(&request.symbol).await?;
839
840        let path = Self::build_api_path("/trade/order");
841
842        // Build order body
843        let mut map = serde_json::Map::new();
844        map.insert(
845            "instId".to_string(),
846            serde_json::Value::String(market.id.clone()),
847        );
848        map.insert(
849            "tdMode".to_string(),
850            serde_json::Value::String(self.options().account_mode.clone()),
851        );
852        map.insert(
853            "side".to_string(),
854            serde_json::Value::String(match request.side {
855                OrderSide::Buy => "buy".to_string(),
856                OrderSide::Sell => "sell".to_string(),
857            }),
858        );
859        map.insert(
860            "ordType".to_string(),
861            serde_json::Value::String(match request.order_type {
862                OrderType::LimitMaker => "post_only".to_string(),
863                OrderType::StopLoss
864                | OrderType::StopMarket
865                | OrderType::TakeProfit
866                | OrderType::TrailingStop => "market".to_string(),
867                _ => "limit".to_string(),
868            }),
869        );
870        map.insert(
871            "sz".to_string(),
872            serde_json::Value::String(request.amount.to_string()),
873        );
874
875        // Add price for limit orders
876        if let Some(p) = request.price {
877            if request.order_type == OrderType::Limit || request.order_type == OrderType::LimitMaker
878            {
879                map.insert("px".to_string(), serde_json::Value::String(p.to_string()));
880            }
881        }
882
883        // Handle time in force - OKX uses ordType for post_only
884        if request.time_in_force == Some(TimeInForce::PO) || request.post_only == Some(true) {
885            // Already handled via ordType = "post_only" above
886        }
887
888        // Handle client order ID
889        if let Some(client_id) = request.client_order_id {
890            map.insert("clOrdId".to_string(), serde_json::Value::String(client_id));
891        }
892
893        // Handle reduce only
894        if let Some(reduce_only) = request.reduce_only {
895            map.insert(
896                "reduceOnly".to_string(),
897                serde_json::Value::Bool(reduce_only),
898            );
899        }
900
901        // Handle stop price / trigger price
902        if let Some(trigger) = request.trigger_price.or(request.stop_price) {
903            map.insert(
904                "triggerPx".to_string(),
905                serde_json::Value::String(trigger.to_string()),
906            );
907        }
908
909        // Handle position side (for hedge mode)
910        if let Some(pos_side) = request.position_side {
911            map.insert("posSide".to_string(), serde_json::Value::String(pos_side));
912        }
913
914        let body = serde_json::Value::Object(map);
915
916        let response = self
917            .signed_request(&path)
918            .method(HttpMethod::Post)
919            .body(body)
920            .execute()
921            .await?;
922
923        let data = response
924            .get("data")
925            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
926
927        // OKX returns array with single order
928        let orders = data.as_array().ok_or_else(|| {
929            Error::from(ParseError::invalid_format(
930                "data",
931                "Expected array of orders",
932            ))
933        })?;
934
935        if orders.is_empty() {
936            return Err(Error::exchange("-1", "No order data returned"));
937        }
938
939        parser::parse_order(&orders[0], Some(&market))
940    }
941
942    /// Create a new order (deprecated).
943    ///
944    /// # Deprecated
945    ///
946    /// This method is deprecated. Use [`create_order_v2`](Self::create_order_v2) with
947    /// [`OrderRequest::builder()`] instead for a more ergonomic API.
948    ///
949    /// # Arguments
950    ///
951    /// * `symbol` - Trading pair symbol.
952    /// * `order_type` - Order type (Market, Limit).
953    /// * `side` - Order side (Buy or Sell).
954    /// * `amount` - Order quantity.
955    /// * `price` - Optional price (required for limit orders).
956    ///
957    /// # Returns
958    ///
959    /// Returns the created [`Order`] structure with order details.
960    #[deprecated(
961        since = "0.2.0",
962        note = "Use create_order_v2 with OrderRequest::builder() instead"
963    )]
964    pub async fn create_order(
965        &self,
966        symbol: &str,
967        order_type: OrderType,
968        side: OrderSide,
969        amount: Amount,
970        price: Option<Price>,
971    ) -> Result<Order> {
972        let market = self.base().market(symbol).await?;
973
974        let path = Self::build_api_path("/trade/order");
975
976        // Build order body
977        let mut map = serde_json::Map::new();
978        map.insert(
979            "instId".to_string(),
980            serde_json::Value::String(market.id.clone()),
981        );
982        map.insert(
983            "tdMode".to_string(),
984            serde_json::Value::String(self.options().account_mode.clone()),
985        );
986        map.insert(
987            "side".to_string(),
988            serde_json::Value::String(match side {
989                OrderSide::Buy => "buy".to_string(),
990                OrderSide::Sell => "sell".to_string(),
991            }),
992        );
993        map.insert(
994            "ordType".to_string(),
995            serde_json::Value::String(match order_type {
996                OrderType::Market => "market".to_string(),
997                OrderType::LimitMaker => "post_only".to_string(),
998                _ => "limit".to_string(),
999            }),
1000        );
1001        map.insert(
1002            "sz".to_string(),
1003            serde_json::Value::String(amount.to_string()),
1004        );
1005
1006        // Add price for limit orders
1007        if let Some(p) = price {
1008            if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
1009                map.insert("px".to_string(), serde_json::Value::String(p.to_string()));
1010            }
1011        }
1012        let body = serde_json::Value::Object(map);
1013
1014        let response = self
1015            .signed_request(&path)
1016            .method(HttpMethod::Post)
1017            .body(body)
1018            .execute()
1019            .await?;
1020
1021        let data = response
1022            .get("data")
1023            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1024
1025        // OKX returns array with single order
1026        let orders = data.as_array().ok_or_else(|| {
1027            Error::from(ParseError::invalid_format(
1028                "data",
1029                "Expected array of orders",
1030            ))
1031        })?;
1032
1033        if orders.is_empty() {
1034            return Err(Error::exchange("-1", "No order data returned"));
1035        }
1036
1037        parser::parse_order(&orders[0], Some(&market))
1038    }
1039
1040    /// Cancel an existing order.
1041    ///
1042    /// # Arguments
1043    ///
1044    /// * `id` - Order ID to cancel.
1045    /// * `symbol` - Trading pair symbol.
1046    ///
1047    /// # Returns
1048    ///
1049    /// Returns the canceled [`Order`] structure.
1050    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
1051        let market = self.base().market(symbol).await?;
1052
1053        let path = Self::build_api_path("/trade/cancel-order");
1054
1055        let mut map = serde_json::Map::new();
1056        map.insert(
1057            "instId".to_string(),
1058            serde_json::Value::String(market.id.clone()),
1059        );
1060        map.insert(
1061            "ordId".to_string(),
1062            serde_json::Value::String(id.to_string()),
1063        );
1064        let body = serde_json::Value::Object(map);
1065
1066        let response = self
1067            .signed_request(&path)
1068            .method(HttpMethod::Post)
1069            .body(body)
1070            .execute()
1071            .await?;
1072
1073        let data = response
1074            .get("data")
1075            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1076
1077        // OKX returns array with single order
1078        let orders = data.as_array().ok_or_else(|| {
1079            Error::from(ParseError::invalid_format(
1080                "data",
1081                "Expected array of orders",
1082            ))
1083        })?;
1084
1085        if orders.is_empty() {
1086            return Err(Error::exchange("-1", "No order data returned"));
1087        }
1088
1089        parser::parse_order(&orders[0], Some(&market))
1090    }
1091
1092    /// Fetch a single order by ID.
1093    ///
1094    /// # Arguments
1095    ///
1096    /// * `id` - Order ID to fetch.
1097    /// * `symbol` - Trading pair symbol.
1098    ///
1099    /// # Returns
1100    ///
1101    /// Returns the [`Order`] structure with current status.
1102    pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
1103        let market = self.base().market(symbol).await?;
1104
1105        let path = Self::build_api_path("/trade/order");
1106
1107        let response = self
1108            .signed_request(&path)
1109            .param("instId", &market.id)
1110            .param("ordId", id)
1111            .execute()
1112            .await?;
1113
1114        let data = response
1115            .get("data")
1116            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1117
1118        // OKX returns array with single order
1119        let orders = data.as_array().ok_or_else(|| {
1120            Error::from(ParseError::invalid_format(
1121                "data",
1122                "Expected array of orders",
1123            ))
1124        })?;
1125
1126        if orders.is_empty() {
1127            return Err(Error::exchange("51400", "Order not found"));
1128        }
1129
1130        parser::parse_order(&orders[0], Some(&market))
1131    }
1132
1133    /// Fetch open orders.
1134    ///
1135    /// # Arguments
1136    ///
1137    /// * `symbol` - Optional trading pair symbol. If None, fetches all open orders.
1138    /// * `since` - Optional start timestamp in milliseconds.
1139    /// * `limit` - Optional limit on number of orders (maximum: 100).
1140    ///
1141    /// # Returns
1142    ///
1143    /// Returns a vector of open [`Order`] structures.
1144    pub async fn fetch_open_orders(
1145        &self,
1146        symbol: Option<&str>,
1147        since: Option<i64>,
1148        limit: Option<u32>,
1149    ) -> Result<Vec<Order>> {
1150        let path = Self::build_api_path("/trade/orders-pending");
1151
1152        // OKX maximum limit is 100
1153        let actual_limit = limit.map_or(100, |l| l.min(100));
1154
1155        let market = if let Some(sym) = symbol {
1156            let m = self.base().market(sym).await?;
1157            Some(m)
1158        } else {
1159            None
1160        };
1161
1162        let mut builder = self
1163            .signed_request(&path)
1164            .param("instType", self.get_inst_type())
1165            .param("limit", actual_limit);
1166
1167        if let Some(ref m) = market {
1168            builder = builder.param("instId", &m.id);
1169        }
1170
1171        if let Some(start_time) = since {
1172            builder = builder.param("begin", start_time);
1173        }
1174
1175        let response = builder.execute().await?;
1176
1177        let data = response
1178            .get("data")
1179            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1180
1181        let orders_array = data.as_array().ok_or_else(|| {
1182            Error::from(ParseError::invalid_format(
1183                "data",
1184                "Expected array of orders",
1185            ))
1186        })?;
1187
1188        let mut orders = Vec::new();
1189        for order_data in orders_array {
1190            match parser::parse_order(order_data, market.as_deref()) {
1191                Ok(order) => orders.push(order),
1192                Err(e) => {
1193                    warn!(error = %e, "Failed to parse open order");
1194                }
1195            }
1196        }
1197
1198        Ok(orders)
1199    }
1200
1201    /// Fetch closed orders.
1202    ///
1203    /// # Arguments
1204    ///
1205    /// * `symbol` - Optional trading pair symbol. If None, fetches all closed orders.
1206    /// * `since` - Optional start timestamp in milliseconds.
1207    /// * `limit` - Optional limit on number of orders (maximum: 100).
1208    ///
1209    /// # Returns
1210    ///
1211    /// Returns a vector of closed [`Order`] structures.
1212    pub async fn fetch_closed_orders(
1213        &self,
1214        symbol: Option<&str>,
1215        since: Option<i64>,
1216        limit: Option<u32>,
1217    ) -> Result<Vec<Order>> {
1218        let path = Self::build_api_path("/trade/orders-history");
1219
1220        // OKX maximum limit is 100
1221        let actual_limit = limit.map_or(100, |l| l.min(100));
1222
1223        let market = if let Some(sym) = symbol {
1224            let m = self.base().market(sym).await?;
1225            Some(m)
1226        } else {
1227            None
1228        };
1229
1230        let mut builder = self
1231            .signed_request(&path)
1232            .param("instType", self.get_inst_type())
1233            .param("limit", actual_limit);
1234
1235        if let Some(ref m) = market {
1236            builder = builder.param("instId", &m.id);
1237        }
1238
1239        if let Some(start_time) = since {
1240            builder = builder.param("begin", start_time);
1241        }
1242
1243        let response = builder.execute().await?;
1244
1245        let data = response
1246            .get("data")
1247            .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
1248
1249        let orders_array = data.as_array().ok_or_else(|| {
1250            Error::from(ParseError::invalid_format(
1251                "data",
1252                "Expected array of orders",
1253            ))
1254        })?;
1255
1256        let mut orders = Vec::new();
1257        for order_data in orders_array {
1258            match parser::parse_order(order_data, market.as_deref()) {
1259                Ok(order) => orders.push(order),
1260                Err(e) => {
1261                    warn!(error = %e, "Failed to parse closed order");
1262                }
1263            }
1264        }
1265
1266        Ok(orders)
1267    }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272    use super::*;
1273
1274    #[test]
1275    fn test_build_api_path() {
1276        let _okx = Okx::builder().build().unwrap();
1277        let path = Okx::build_api_path("/public/instruments");
1278        assert_eq!(path, "/api/v5/public/instruments");
1279    }
1280
1281    #[test]
1282    fn test_get_inst_type_spot() {
1283        let okx = Okx::builder().build().unwrap();
1284        let inst_type = okx.get_inst_type();
1285        assert_eq!(inst_type, "SPOT");
1286    }
1287
1288    #[test]
1289    fn test_get_inst_type_margin() {
1290        use ccxt_core::types::default_type::DefaultType;
1291        let okx = Okx::builder()
1292            .default_type(DefaultType::Margin)
1293            .build()
1294            .unwrap();
1295        let inst_type = okx.get_inst_type();
1296        assert_eq!(inst_type, "MARGIN");
1297    }
1298
1299    #[test]
1300    fn test_get_inst_type_swap() {
1301        use ccxt_core::types::default_type::DefaultType;
1302        let okx = Okx::builder()
1303            .default_type(DefaultType::Swap)
1304            .build()
1305            .unwrap();
1306        let inst_type = okx.get_inst_type();
1307        assert_eq!(inst_type, "SWAP");
1308    }
1309
1310    #[test]
1311    fn test_get_inst_type_futures() {
1312        use ccxt_core::types::default_type::DefaultType;
1313        let okx = Okx::builder()
1314            .default_type(DefaultType::Futures)
1315            .build()
1316            .unwrap();
1317        let inst_type = okx.get_inst_type();
1318        assert_eq!(inst_type, "FUTURES");
1319    }
1320
1321    #[test]
1322    fn test_get_inst_type_option() {
1323        use ccxt_core::types::default_type::DefaultType;
1324        let okx = Okx::builder()
1325            .default_type(DefaultType::Option)
1326            .build()
1327            .unwrap();
1328        let inst_type = okx.get_inst_type();
1329        assert_eq!(inst_type, "OPTION");
1330    }
1331
1332    #[test]
1333    fn test_get_timestamp() {
1334        let _okx = Okx::builder().build().unwrap();
1335        let ts = Okx::get_timestamp();
1336
1337        // Should be in ISO 8601 format
1338        assert!(ts.contains("T"));
1339        assert!(ts.contains("Z"));
1340        assert!(ts.len() > 20);
1341    }
1342}