1use super::{Bybit, BybitAuth, error, parser};
6use ccxt_core::{
7 Error, ParseError, Result,
8 types::{
9 Amount, Balance, Market, OHLCV, OhlcvRequest, Order, OrderBook, OrderRequest, OrderSide,
10 OrderType, Price, Ticker, TimeInForce, Trade,
11 },
12};
13use reqwest::header::{HeaderMap, HeaderValue};
14use serde_json::Value;
15use std::{collections::HashMap, sync::Arc};
16use tracing::{debug, info, warn};
17
18impl Bybit {
19 #[deprecated(
30 since = "0.1.0",
31 note = "Use `signed_request()` builder instead which handles timestamps internally"
32 )]
33 #[allow(dead_code)]
34 fn get_timestamp() -> String {
35 chrono::Utc::now().timestamp_millis().to_string()
36 }
37
38 pub fn get_auth(&self) -> Result<BybitAuth> {
40 let config = &self.base().config;
41
42 let api_key = config
43 .api_key
44 .as_ref()
45 .ok_or_else(|| Error::authentication("API key is required"))?;
46 let secret = config
47 .secret
48 .as_ref()
49 .ok_or_else(|| Error::authentication("API secret is required"))?;
50
51 Ok(BybitAuth::new(
52 api_key.expose_secret().to_string(),
53 secret.expose_secret().to_string(),
54 ))
55 }
56
57 pub fn check_required_credentials(&self) -> Result<()> {
59 self.base().check_required_credentials()
60 }
61
62 fn build_api_path(endpoint: &str) -> String {
64 format!("/v5{}", endpoint)
65 }
66
67 fn get_category(&self) -> &str {
69 match self.options().account_type.as_str() {
70 "CONTRACT" | "LINEAR" => "linear",
71 "INVERSE" => "inverse",
72 "OPTION" => "option",
73 _ => "spot",
74 }
75 }
76
77 async fn public_request(
79 &self,
80 method: &str,
81 path: &str,
82 params: Option<&HashMap<String, String>>,
83 ) -> Result<Value> {
84 let urls = self.urls();
85 let mut url = format!("{}{}", urls.rest, path);
86
87 if let Some(p) = params {
88 if !p.is_empty() {
89 let query: Vec<String> = p
90 .iter()
91 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
92 .collect();
93 url = format!("{}?{}", url, query.join("&"));
94 }
95 }
96
97 debug!("Bybit public request: {} {}", method, url);
98
99 let response = match method.to_uppercase().as_str() {
100 "GET" => self.base().http_client.get(&url, None).await?,
101 "POST" => self.base().http_client.post(&url, None, None).await?,
102 _ => {
103 return Err(Error::invalid_request(format!(
104 "Unsupported HTTP method: {}",
105 method
106 )));
107 }
108 };
109
110 if error::is_error_response(&response) {
112 return Err(error::parse_error(&response));
113 }
114
115 Ok(response)
116 }
117
118 #[deprecated(
126 since = "0.1.0",
127 note = "Use `signed_request()` builder instead for cleaner, more maintainable code"
128 )]
129 #[allow(dead_code)]
130 #[allow(deprecated)]
131 async fn private_request(
132 &self,
133 method: &str,
134 path: &str,
135 params: Option<&HashMap<String, String>>,
136 body: Option<&Value>,
137 ) -> Result<Value> {
138 self.check_required_credentials()?;
139
140 let auth = self.get_auth()?;
141 let urls = self.urls();
142 let timestamp = Self::get_timestamp();
143 let recv_window = self.options().recv_window;
144
145 let query_string = if let Some(p) = params {
147 if p.is_empty() {
148 String::new()
149 } else {
150 let query: Vec<String> = p
151 .iter()
152 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
153 .collect();
154 query.join("&")
155 }
156 } else {
157 String::new()
158 };
159
160 let body_string = match body {
162 Some(b) => serde_json::to_string(b).map_err(|e| {
163 ccxt_core::Error::from(ccxt_core::ParseError::invalid_format(
164 "request body",
165 format!("JSON serialization failed: {}", e),
166 ))
167 })?,
168 None => String::new(),
169 };
170
171 let sign_params = if method.to_uppercase() == "GET" {
173 &query_string
174 } else {
175 &body_string
176 };
177 let signature = auth.sign(×tamp, recv_window, sign_params);
178
179 let mut headers = HeaderMap::new();
181 auth.add_auth_headers(&mut headers, ×tamp, &signature, recv_window);
182 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
183
184 let url = if query_string.is_empty() {
185 format!("{}{}", urls.rest, path)
186 } else {
187 format!("{}{}?{}", urls.rest, path, query_string)
188 };
189 debug!("Bybit private request: {} {}", method, url);
190
191 let response = match method.to_uppercase().as_str() {
192 "GET" => self.base().http_client.get(&url, Some(headers)).await?,
193 "POST" => {
194 let body_value = body.cloned();
195 self.base()
196 .http_client
197 .post(&url, Some(headers), body_value)
198 .await?
199 }
200 "DELETE" => {
201 self.base()
202 .http_client
203 .delete(&url, Some(headers), None)
204 .await?
205 }
206 _ => {
207 return Err(Error::invalid_request(format!(
208 "Unsupported HTTP method: {}",
209 method
210 )));
211 }
212 };
213
214 if error::is_error_response(&response) {
216 return Err(error::parse_error(&response));
217 }
218
219 Ok(response)
220 }
221
222 pub async fn fetch_markets(&self) -> Result<Arc<HashMap<String, Arc<Market>>>> {
248 let path = Self::build_api_path("/market/instruments-info");
249 let mut params = HashMap::new();
250 params.insert("category".to_string(), self.get_category().to_string());
251
252 let response = self.public_request("GET", &path, Some(¶ms)).await?;
253
254 let result = response
255 .get("result")
256 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
257
258 let list = result
259 .get("list")
260 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
261
262 let instruments = list.as_array().ok_or_else(|| {
263 Error::from(ParseError::invalid_format(
264 "list",
265 "Expected array of instruments",
266 ))
267 })?;
268
269 let mut markets = Vec::new();
270 for instrument in instruments {
271 match parser::parse_market(instrument) {
272 Ok(market) => markets.push(market),
273 Err(e) => {
274 warn!(error = %e, "Failed to parse market");
275 }
276 }
277 }
278
279 let markets = self.base().set_markets(markets, None).await?;
281
282 info!("Loaded {} markets for Bybit", markets.len());
283 Ok(markets)
284 }
285
286 pub async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
298 let _loading_guard = self.base().market_loading_lock.lock().await;
301
302 {
304 let cache = self.base().market_cache.read().await;
305 if cache.is_loaded() && !reload {
306 debug!(
307 "Returning cached markets for Bybit ({} markets)",
308 cache.market_count()
309 );
310 return Ok(cache.markets());
311 }
312 }
313
314 info!("Loading markets for Bybit (reload: {})", reload);
315 let _markets = self.fetch_markets().await?;
316
317 let cache = self.base().market_cache.read().await;
318 Ok(cache.markets())
319 }
320
321 pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
331 let market = self.base().market(symbol).await?;
332
333 let path = Self::build_api_path("/market/tickers");
334 let mut params = HashMap::new();
335 params.insert("category".to_string(), self.get_category().to_string());
336 params.insert("symbol".to_string(), market.id.clone());
337
338 let response = self.public_request("GET", &path, Some(¶ms)).await?;
339
340 let result = response
341 .get("result")
342 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
343
344 let list = result
345 .get("list")
346 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
347
348 let tickers = list.as_array().ok_or_else(|| {
349 Error::from(ParseError::invalid_format(
350 "list",
351 "Expected array of tickers",
352 ))
353 })?;
354
355 if tickers.is_empty() {
356 return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
357 }
358
359 parser::parse_ticker(&tickers[0], Some(&market))
360 }
361
362 pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
372 let cache = self.base().market_cache.read().await;
373 if !cache.is_loaded() {
374 drop(cache);
375 return Err(Error::exchange(
376 "-1",
377 "Markets not loaded. Call load_markets() first.",
378 ));
379 }
380 let markets_snapshot: std::collections::HashMap<String, Arc<Market>> = cache
382 .iter_markets()
383 .map(|(_, m)| (m.id.clone(), m))
384 .collect();
385 drop(cache);
386
387 let path = Self::build_api_path("/market/tickers");
388 let mut params = HashMap::new();
389 params.insert("category".to_string(), self.get_category().to_string());
390
391 let response = self.public_request("GET", &path, Some(¶ms)).await?;
392
393 let result = response
394 .get("result")
395 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
396
397 let list = result
398 .get("list")
399 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
400
401 let tickers_array = list.as_array().ok_or_else(|| {
402 Error::from(ParseError::invalid_format(
403 "list",
404 "Expected array of tickers",
405 ))
406 })?;
407
408 let mut tickers = Vec::new();
409 for ticker_data in tickers_array {
410 if let Some(symbol_id) = ticker_data["symbol"].as_str() {
411 if let Some(market) = markets_snapshot.get(symbol_id) {
412 match parser::parse_ticker(ticker_data, Some(market)) {
413 Ok(ticker) => {
414 if let Some(ref syms) = symbols {
415 if syms.contains(&ticker.symbol) {
416 tickers.push(ticker);
417 }
418 } else {
419 tickers.push(ticker);
420 }
421 }
422 Err(e) => {
423 warn!(
424 error = %e,
425 symbol = %symbol_id,
426 "Failed to parse ticker"
427 );
428 }
429 }
430 }
431 }
432 }
433
434 Ok(tickers)
435 }
436
437 pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
452 let market = self.base().market(symbol).await?;
453
454 let path = Self::build_api_path("/market/orderbook");
455 let mut params = HashMap::new();
456 params.insert("category".to_string(), self.get_category().to_string());
457 params.insert("symbol".to_string(), market.id.clone());
458
459 let actual_limit = limit.map_or(25, |l| l.min(500));
462 params.insert("limit".to_string(), actual_limit.to_string());
463
464 let response = self.public_request("GET", &path, Some(¶ms)).await?;
465
466 let result = response
467 .get("result")
468 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
469
470 parser::parse_orderbook(result, market.symbol.clone())
471 }
472
473 pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
484 let market = self.base().market(symbol).await?;
485
486 let path = Self::build_api_path("/market/recent-trade");
487 let mut params = HashMap::new();
488 params.insert("category".to_string(), self.get_category().to_string());
489 params.insert("symbol".to_string(), market.id.clone());
490
491 let actual_limit = limit.map_or(60, |l| l.min(1000));
493 params.insert("limit".to_string(), actual_limit.to_string());
494
495 let response = self.public_request("GET", &path, Some(¶ms)).await?;
496
497 let result = response
498 .get("result")
499 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
500
501 let list = result
502 .get("list")
503 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
504
505 let trades_array = list.as_array().ok_or_else(|| {
506 Error::from(ParseError::invalid_format(
507 "list",
508 "Expected array of trades",
509 ))
510 })?;
511
512 let mut trades = Vec::new();
513 for trade_data in trades_array {
514 match parser::parse_trade(trade_data, Some(&market)) {
515 Ok(trade) => trades.push(trade),
516 Err(e) => {
517 warn!(error = %e, "Failed to parse trade");
518 }
519 }
520 }
521
522 trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
524
525 Ok(trades)
526 }
527
528 pub async fn fetch_ohlcv_v2(&self, request: OhlcvRequest) -> Result<Vec<OHLCV>> {
547 let market = self.base().market(&request.symbol).await?;
548
549 let timeframes = self.timeframes();
551 let bybit_timeframe = timeframes.get(&request.timeframe).ok_or_else(|| {
552 Error::invalid_request(format!("Unsupported timeframe: {}", request.timeframe))
553 })?;
554
555 let path = Self::build_api_path("/market/kline");
556 let mut params = HashMap::new();
557 params.insert("category".to_string(), self.get_category().to_string());
558 params.insert("symbol".to_string(), market.id.clone());
559 params.insert("interval".to_string(), bybit_timeframe.clone());
560
561 let actual_limit = request.limit.map_or(200, |l| l.min(1000));
563 params.insert("limit".to_string(), actual_limit.to_string());
564
565 if let Some(start_time) = request.since {
566 params.insert("start".to_string(), start_time.to_string());
567 }
568
569 if let Some(end_time) = request.until {
570 params.insert("end".to_string(), end_time.to_string());
571 }
572
573 let response = self.public_request("GET", &path, Some(¶ms)).await?;
574
575 let result = response
576 .get("result")
577 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
578
579 let list = result
580 .get("list")
581 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
582
583 let candles_array = list.as_array().ok_or_else(|| {
584 Error::from(ParseError::invalid_format(
585 "list",
586 "Expected array of candles",
587 ))
588 })?;
589
590 let mut ohlcv = Vec::new();
591 for candle_data in candles_array {
592 match parser::parse_ohlcv(candle_data) {
593 Ok(candle) => ohlcv.push(candle),
594 Err(e) => {
595 warn!(error = %e, "Failed to parse OHLCV");
596 }
597 }
598 }
599
600 ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
602
603 Ok(ohlcv)
604 }
605
606 #[deprecated(
624 since = "0.2.0",
625 note = "Use fetch_ohlcv_v2 with OhlcvRequest::builder() instead"
626 )]
627 pub async fn fetch_ohlcv(
628 &self,
629 symbol: &str,
630 timeframe: &str,
631 since: Option<i64>,
632 limit: Option<u32>,
633 ) -> Result<Vec<OHLCV>> {
634 let market = self.base().market(symbol).await?;
635
636 let timeframes = self.timeframes();
638 let bybit_timeframe = timeframes.get(timeframe).ok_or_else(|| {
639 Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
640 })?;
641
642 let path = Self::build_api_path("/market/kline");
643 let mut params = HashMap::new();
644 params.insert("category".to_string(), self.get_category().to_string());
645 params.insert("symbol".to_string(), market.id.clone());
646 params.insert("interval".to_string(), bybit_timeframe.clone());
647
648 let actual_limit = limit.map_or(200, |l| l.min(1000));
650 params.insert("limit".to_string(), actual_limit.to_string());
651
652 if let Some(start_time) = since {
653 params.insert("start".to_string(), start_time.to_string());
654 }
655
656 let response = self.public_request("GET", &path, Some(¶ms)).await?;
657
658 let result = response
659 .get("result")
660 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
661
662 let list = result
663 .get("list")
664 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
665
666 let candles_array = list.as_array().ok_or_else(|| {
667 Error::from(ParseError::invalid_format(
668 "list",
669 "Expected array of candles",
670 ))
671 })?;
672
673 let mut ohlcv = Vec::new();
674 for candle_data in candles_array {
675 match parser::parse_ohlcv(candle_data) {
676 Ok(candle) => ohlcv.push(candle),
677 Err(e) => {
678 warn!(error = %e, "Failed to parse OHLCV");
679 }
680 }
681 }
682
683 ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
685
686 Ok(ohlcv)
687 }
688
689 pub async fn fetch_balance(&self) -> Result<Balance> {
703 let path = Self::build_api_path("/account/wallet-balance");
704
705 let response = self
706 .signed_request(&path)
707 .param("accountType", &self.options().account_type)
708 .execute()
709 .await?;
710
711 let result = response
712 .get("result")
713 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
714
715 parser::parse_balance(result)
716 }
717
718 pub async fn fetch_my_trades(
730 &self,
731 symbol: &str,
732 since: Option<i64>,
733 limit: Option<u32>,
734 ) -> Result<Vec<Trade>> {
735 let market = self.base().market(symbol).await?;
736
737 let path = Self::build_api_path("/execution/list");
738
739 let actual_limit = limit.map_or(50, |l| l.min(100));
741
742 let response = self
743 .signed_request(&path)
744 .param("category", self.get_category())
745 .param("symbol", &market.id)
746 .param("limit", actual_limit)
747 .optional_param("startTime", since)
748 .execute()
749 .await?;
750
751 let result = response
752 .get("result")
753 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
754
755 let list = result
756 .get("list")
757 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
758
759 let trades_array = list.as_array().ok_or_else(|| {
760 Error::from(ParseError::invalid_format(
761 "list",
762 "Expected array of trades",
763 ))
764 })?;
765
766 let mut trades = Vec::new();
767 for trade_data in trades_array {
768 match parser::parse_trade(trade_data, Some(&market)) {
769 Ok(trade) => trades.push(trade),
770 Err(e) => {
771 warn!(error = %e, "Failed to parse my trade");
772 }
773 }
774 }
775
776 Ok(trades)
777 }
778
779 pub async fn create_order_v2(&self, request: OrderRequest) -> Result<Order> {
799 use crate::bybit::signed_request::HttpMethod;
800
801 let market = self.base().market(&request.symbol).await?;
802
803 let path = Self::build_api_path("/order/create");
804
805 let mut map = serde_json::Map::new();
807 map.insert(
808 "category".to_string(),
809 serde_json::Value::String(self.get_category().to_string()),
810 );
811 map.insert(
812 "symbol".to_string(),
813 serde_json::Value::String(market.id.clone()),
814 );
815 map.insert(
816 "side".to_string(),
817 serde_json::Value::String(match request.side {
818 OrderSide::Buy => "Buy".to_string(),
819 OrderSide::Sell => "Sell".to_string(),
820 }),
821 );
822 map.insert(
823 "orderType".to_string(),
824 serde_json::Value::String(match request.order_type {
825 OrderType::Market
826 | OrderType::StopLoss
827 | OrderType::StopMarket
828 | OrderType::TakeProfit
829 | OrderType::TrailingStop => "Market".to_string(),
830 _ => "Limit".to_string(),
831 }),
832 );
833 map.insert(
834 "qty".to_string(),
835 serde_json::Value::String(request.amount.to_string()),
836 );
837
838 if let Some(p) = request.price {
840 if request.order_type == OrderType::Limit || request.order_type == OrderType::LimitMaker
841 {
842 map.insert(
843 "price".to_string(),
844 serde_json::Value::String(p.to_string()),
845 );
846 }
847 }
848
849 if let Some(tif) = request.time_in_force {
851 let tif_str = match tif {
852 TimeInForce::GTC => "GTC",
853 TimeInForce::IOC => "IOC",
854 TimeInForce::FOK => "FOK",
855 TimeInForce::PO => "PostOnly",
856 };
857 map.insert(
858 "timeInForce".to_string(),
859 serde_json::Value::String(tif_str.to_string()),
860 );
861 } else if request.order_type == OrderType::LimitMaker || request.post_only == Some(true) {
862 map.insert(
863 "timeInForce".to_string(),
864 serde_json::Value::String("PostOnly".to_string()),
865 );
866 }
867
868 if let Some(client_id) = request.client_order_id {
870 map.insert(
871 "orderLinkId".to_string(),
872 serde_json::Value::String(client_id),
873 );
874 }
875
876 if let Some(reduce_only) = request.reduce_only {
878 map.insert(
879 "reduceOnly".to_string(),
880 serde_json::Value::Bool(reduce_only),
881 );
882 }
883
884 if let Some(trigger) = request.trigger_price.or(request.stop_price) {
886 map.insert(
887 "triggerPrice".to_string(),
888 serde_json::Value::String(trigger.to_string()),
889 );
890 }
891
892 if let Some(tp) = request.take_profit_price {
894 map.insert(
895 "takeProfit".to_string(),
896 serde_json::Value::String(tp.to_string()),
897 );
898 }
899
900 if let Some(sl) = request.stop_loss_price {
902 map.insert(
903 "stopLoss".to_string(),
904 serde_json::Value::String(sl.to_string()),
905 );
906 }
907
908 if let Some(pos_side) = request.position_side {
910 map.insert(
911 "positionIdx".to_string(),
912 serde_json::Value::String(match pos_side.as_str() {
913 "LONG" => "1".to_string(),
914 "SHORT" => "2".to_string(),
915 _ => "0".to_string(), }),
917 );
918 }
919
920 let body = serde_json::Value::Object(map);
921
922 let response = self
923 .signed_request(&path)
924 .method(HttpMethod::Post)
925 .body(body)
926 .execute()
927 .await?;
928
929 let result = response
930 .get("result")
931 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
932
933 parser::parse_order(result, Some(&market))
934 }
935
936 #[deprecated(
955 since = "0.2.0",
956 note = "Use create_order_v2 with OrderRequest::builder() instead"
957 )]
958 pub async fn create_order(
959 &self,
960 symbol: &str,
961 order_type: OrderType,
962 side: OrderSide,
963 amount: Amount,
964 price: Option<Price>,
965 ) -> Result<Order> {
966 use crate::bybit::signed_request::HttpMethod;
967
968 let market = self.base().market(symbol).await?;
969
970 let path = Self::build_api_path("/order/create");
971
972 let mut map = serde_json::Map::new();
974 map.insert(
975 "category".to_string(),
976 serde_json::Value::String(self.get_category().to_string()),
977 );
978 map.insert(
979 "symbol".to_string(),
980 serde_json::Value::String(market.id.clone()),
981 );
982 map.insert(
983 "side".to_string(),
984 serde_json::Value::String(match side {
985 OrderSide::Buy => "Buy".to_string(),
986 OrderSide::Sell => "Sell".to_string(),
987 }),
988 );
989 map.insert(
990 "orderType".to_string(),
991 serde_json::Value::String(match order_type {
992 OrderType::Market => "Market".to_string(),
993 _ => "Limit".to_string(),
994 }),
995 );
996 map.insert(
997 "qty".to_string(),
998 serde_json::Value::String(amount.to_string()),
999 );
1000
1001 if let Some(p) = price {
1003 if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
1004 map.insert(
1005 "price".to_string(),
1006 serde_json::Value::String(p.to_string()),
1007 );
1008 }
1009 }
1010
1011 if order_type == OrderType::LimitMaker {
1013 map.insert(
1014 "timeInForce".to_string(),
1015 serde_json::Value::String("PostOnly".to_string()),
1016 );
1017 }
1018
1019 let body = serde_json::Value::Object(map);
1020
1021 let response = self
1022 .signed_request(&path)
1023 .method(HttpMethod::Post)
1024 .body(body)
1025 .execute()
1026 .await?;
1027
1028 let result = response
1029 .get("result")
1030 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1031
1032 parser::parse_order(result, Some(&market))
1033 }
1034
1035 pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
1046 use crate::bybit::signed_request::HttpMethod;
1047
1048 let market = self.base().market(symbol).await?;
1049
1050 let path = Self::build_api_path("/order/cancel");
1051
1052 let mut map = serde_json::Map::new();
1053 map.insert(
1054 "category".to_string(),
1055 serde_json::Value::String(self.get_category().to_string()),
1056 );
1057 map.insert(
1058 "symbol".to_string(),
1059 serde_json::Value::String(market.id.clone()),
1060 );
1061 map.insert(
1062 "orderId".to_string(),
1063 serde_json::Value::String(id.to_string()),
1064 );
1065 let body = serde_json::Value::Object(map);
1066
1067 let response = self
1068 .signed_request(&path)
1069 .method(HttpMethod::Post)
1070 .body(body)
1071 .execute()
1072 .await?;
1073
1074 let result = response
1075 .get("result")
1076 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1077
1078 parser::parse_order(result, Some(&market))
1079 }
1080
1081 pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
1092 let market = self.base().market(symbol).await?;
1093
1094 let path = Self::build_api_path("/order/realtime");
1095
1096 let response = self
1097 .signed_request(&path)
1098 .param("category", self.get_category())
1099 .param("symbol", &market.id)
1100 .param("orderId", id)
1101 .execute()
1102 .await?;
1103
1104 let result = response
1105 .get("result")
1106 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1107
1108 let list = result
1109 .get("list")
1110 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1111
1112 let orders = list.as_array().ok_or_else(|| {
1113 Error::from(ParseError::invalid_format(
1114 "list",
1115 "Expected array of orders",
1116 ))
1117 })?;
1118
1119 if orders.is_empty() {
1120 return Err(Error::exchange("110008", "Order not found"));
1121 }
1122
1123 parser::parse_order(&orders[0], Some(&market))
1124 }
1125
1126 pub async fn fetch_open_orders(
1138 &self,
1139 symbol: Option<&str>,
1140 since: Option<i64>,
1141 limit: Option<u32>,
1142 ) -> Result<Vec<Order>> {
1143 let path = Self::build_api_path("/order/realtime");
1144
1145 let actual_limit = limit.map_or(50, |l| l.min(50));
1147
1148 let market = if let Some(sym) = symbol {
1149 Some(self.base().market(sym).await?)
1150 } else {
1151 None
1152 };
1153
1154 let mut builder = self
1155 .signed_request(&path)
1156 .param("category", self.get_category())
1157 .param("limit", actual_limit)
1158 .optional_param("startTime", since);
1159
1160 if let Some(ref m) = market {
1161 builder = builder.param("symbol", &m.id);
1162 }
1163
1164 let response = builder.execute().await?;
1165
1166 let result = response
1167 .get("result")
1168 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1169
1170 let list = result
1171 .get("list")
1172 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1173
1174 let orders_array = list.as_array().ok_or_else(|| {
1175 Error::from(ParseError::invalid_format(
1176 "list",
1177 "Expected array of orders",
1178 ))
1179 })?;
1180
1181 let mut orders = Vec::new();
1182 for order_data in orders_array {
1183 match parser::parse_order(order_data, market.as_deref()) {
1184 Ok(order) => orders.push(order),
1185 Err(e) => {
1186 warn!(error = %e, "Failed to parse open order");
1187 }
1188 }
1189 }
1190
1191 Ok(orders)
1192 }
1193
1194 pub async fn fetch_closed_orders(
1206 &self,
1207 symbol: Option<&str>,
1208 since: Option<i64>,
1209 limit: Option<u32>,
1210 ) -> Result<Vec<Order>> {
1211 let path = Self::build_api_path("/order/history");
1212
1213 let actual_limit = limit.map_or(50, |l| l.min(50));
1215
1216 let market = if let Some(sym) = symbol {
1217 Some(self.base().market(sym).await?)
1218 } else {
1219 None
1220 };
1221
1222 let mut builder = self
1223 .signed_request(&path)
1224 .param("category", self.get_category())
1225 .param("limit", actual_limit)
1226 .optional_param("startTime", since);
1227
1228 if let Some(ref m) = market {
1229 builder = builder.param("symbol", &m.id);
1230 }
1231
1232 let response = builder.execute().await?;
1233
1234 let result = response
1235 .get("result")
1236 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
1237
1238 let list = result
1239 .get("list")
1240 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
1241
1242 let orders_array = list.as_array().ok_or_else(|| {
1243 Error::from(ParseError::invalid_format(
1244 "list",
1245 "Expected array of orders",
1246 ))
1247 })?;
1248
1249 let mut orders = Vec::new();
1250 for order_data in orders_array {
1251 match parser::parse_order(order_data, market.as_deref()) {
1252 Ok(order) => orders.push(order),
1253 Err(e) => {
1254 warn!(error = %e, "Failed to parse closed order");
1255 }
1256 }
1257 }
1258
1259 Ok(orders)
1260 }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 #![allow(clippy::disallowed_methods)]
1266 use super::*;
1267
1268 #[test]
1269 fn test_build_api_path() {
1270 let path = Bybit::build_api_path("/market/instruments-info");
1271 assert_eq!(path, "/v5/market/instruments-info");
1272 }
1273
1274 #[test]
1275 fn test_get_category_spot() {
1276 let bybit = Bybit::builder().build().unwrap();
1277 let category = bybit.get_category();
1278 assert_eq!(category, "spot");
1279 }
1280
1281 #[test]
1282 fn test_get_category_linear() {
1283 let bybit = Bybit::builder().account_type("LINEAR").build().unwrap();
1284 let category = bybit.get_category();
1285 assert_eq!(category, "linear");
1286 }
1287
1288 #[test]
1289 #[allow(deprecated)]
1290 fn test_get_timestamp() {
1291 let _bybit = Bybit::builder().build().unwrap();
1292 let ts = Bybit::get_timestamp();
1293
1294 assert!(!ts.is_empty());
1296 let parsed: i64 = ts.parse().unwrap();
1297 assert!(parsed > 0);
1298 }
1299}