1use 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 #[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 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 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 fn build_api_path(endpoint: &str) -> String {
76 format!("/api/v5{}", endpoint)
77 }
78
79 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 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 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 if error::is_error_response(&response) {
160 return Err(error::parse_error(&response));
161 }
162
163 Ok(response)
164 }
165
166 #[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 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 let body_string = body
209 .map(|b| serde_json::to_string(b).unwrap_or_default())
210 .unwrap_or_default();
211
212 let sign_path = format!("{}{}", path, query_string);
214 let signature = auth.sign(×tamp, method, &sign_path, &body_string);
215
216 let mut headers = HeaderMap::new();
218 auth.add_auth_headers(&mut headers, ×tamp, &signature);
219 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
220
221 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 if error::is_error_response(&response) {
254 return Err(error::parse_error(&response));
255 }
256
257 Ok(response)
258 }
259
260 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(¶ms)).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 let result = self.base().set_markets(markets, None).await?;
315
316 info!("Loaded {} markets for OKX", result.len());
317 Ok(result)
318 }
319
320 pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
332 let _loading_guard = self.base().market_loading_lock.lock().await;
335
336 {
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 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(¶ms)).await?;
372
373 let data = response
374 .get("data")
375 .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
376
377 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 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(¶ms)).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 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 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(¶ms)).await?;
491
492 let data = response
493 .get("data")
494 .ok_or_else(|| Error::from(ParseError::missing_field("data")))?;
495
496 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 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 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(¶ms)).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 trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
560
561 Ok(trades)
562 }
563
564 pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
583 let market = self.base().market(&request.symbol).await?;
584
585 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 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(¶ms)).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 ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
633
634 Ok(ohlcv)
635 }
636
637 #[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 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 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(¶ms)).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 ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
711
712 Ok(ohlcv)
713 }
714
715 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 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 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 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 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 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 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 if request.time_in_force == Some(TimeInForce::PO) || request.post_only == Some(true) {
885 }
887
888 if let Some(client_id) = request.client_order_id {
890 map.insert("clOrdId".to_string(), serde_json::Value::String(client_id));
891 }
892
893 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 assert!(ts.contains("T"));
1339 assert!(ts.contains("Z"));
1340 assert!(ts.len() > 20);
1341 }
1342}