1use super::{Bybit, BybitAuth, error, parser};
6use ccxt_core::{
7 Error, ParseError, Result,
8 types::{Balance, Market, OHLCV, Order, OrderBook, OrderSide, OrderType, Ticker, Trade},
9};
10use reqwest::header::{HeaderMap, HeaderValue};
11use serde_json::Value;
12use std::{collections::HashMap, sync::Arc};
13use tracing::{debug, info, warn};
14
15impl Bybit {
16 fn get_timestamp(&self) -> String {
22 chrono::Utc::now().timestamp_millis().to_string()
23 }
24
25 fn get_auth(&self) -> Result<BybitAuth> {
27 let config = &self.base().config;
28
29 let api_key = config
30 .api_key
31 .as_ref()
32 .ok_or_else(|| Error::authentication("API key is required"))?;
33 let secret = config
34 .secret
35 .as_ref()
36 .ok_or_else(|| Error::authentication("API secret is required"))?;
37
38 Ok(BybitAuth::new(api_key.clone(), secret.clone()))
39 }
40
41 pub fn check_required_credentials(&self) -> Result<()> {
43 self.base().check_required_credentials()
44 }
45
46 fn build_api_path(&self, endpoint: &str) -> String {
48 format!("/v5{}", endpoint)
49 }
50
51 fn get_category(&self) -> &str {
53 match self.options().account_type.as_str() {
54 "SPOT" => "spot",
55 "CONTRACT" | "LINEAR" => "linear",
56 "INVERSE" => "inverse",
57 "OPTION" => "option",
58 _ => "spot",
59 }
60 }
61
62 async fn public_request(
64 &self,
65 method: &str,
66 path: &str,
67 params: Option<&HashMap<String, String>>,
68 ) -> Result<Value> {
69 let urls = self.urls();
70 let mut url = format!("{}{}", urls.rest, path);
71
72 if let Some(p) = params {
73 if !p.is_empty() {
74 let query: Vec<String> = p
75 .iter()
76 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
77 .collect();
78 url = format!("{}?{}", url, query.join("&"));
79 }
80 }
81
82 debug!("Bybit public request: {} {}", method, url);
83
84 let response = match method.to_uppercase().as_str() {
85 "GET" => self.base().http_client.get(&url, None).await?,
86 "POST" => self.base().http_client.post(&url, None, None).await?,
87 _ => {
88 return Err(Error::invalid_request(format!(
89 "Unsupported HTTP method: {}",
90 method
91 )));
92 }
93 };
94
95 if error::is_error_response(&response) {
97 return Err(error::parse_error(&response));
98 }
99
100 Ok(response)
101 }
102
103 async fn private_request(
105 &self,
106 method: &str,
107 path: &str,
108 params: Option<&HashMap<String, String>>,
109 body: Option<&Value>,
110 ) -> Result<Value> {
111 self.check_required_credentials()?;
112
113 let auth = self.get_auth()?;
114 let urls = self.urls();
115 let timestamp = self.get_timestamp();
116 let recv_window = self.options().recv_window;
117
118 let query_string = if let Some(p) = params {
120 if !p.is_empty() {
121 let query: Vec<String> = p
122 .iter()
123 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
124 .collect();
125 query.join("&")
126 } else {
127 String::new()
128 }
129 } else {
130 String::new()
131 };
132
133 let body_string = body
135 .map(|b| serde_json::to_string(b).unwrap_or_default())
136 .unwrap_or_default();
137
138 let sign_params = if method.to_uppercase() == "GET" {
140 &query_string
141 } else {
142 &body_string
143 };
144 let signature = auth.sign(×tamp, recv_window, sign_params);
145
146 let mut headers = HeaderMap::new();
148 auth.add_auth_headers(&mut headers, ×tamp, &signature, recv_window);
149 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
150
151 let url = if query_string.is_empty() {
152 format!("{}{}", urls.rest, path)
153 } else {
154 format!("{}{}?{}", urls.rest, path, query_string)
155 };
156 debug!("Bybit private request: {} {}", method, url);
157
158 let response = match method.to_uppercase().as_str() {
159 "GET" => self.base().http_client.get(&url, Some(headers)).await?,
160 "POST" => {
161 let body_value = body.cloned();
162 self.base()
163 .http_client
164 .post(&url, Some(headers), body_value)
165 .await?
166 }
167 "DELETE" => {
168 self.base()
169 .http_client
170 .delete(&url, Some(headers), None)
171 .await?
172 }
173 _ => {
174 return Err(Error::invalid_request(format!(
175 "Unsupported HTTP method: {}",
176 method
177 )));
178 }
179 };
180
181 if error::is_error_response(&response) {
183 return Err(error::parse_error(&response));
184 }
185
186 Ok(response)
187 }
188
189 pub async fn fetch_markets(&self) -> Result<HashMap<String, Arc<Market>>> {
215 let path = self.build_api_path("/market/instruments-info");
216 let mut params = HashMap::new();
217 params.insert("category".to_string(), self.get_category().to_string());
218
219 let response = self.public_request("GET", &path, Some(¶ms)).await?;
220
221 let result = response
222 .get("result")
223 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
224
225 let list = result
226 .get("list")
227 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
228
229 let instruments = list.as_array().ok_or_else(|| {
230 Error::from(ParseError::invalid_format(
231 "list",
232 "Expected array of instruments",
233 ))
234 })?;
235
236 let mut markets = Vec::new();
237 for instrument in instruments {
238 match parser::parse_market(instrument) {
239 Ok(market) => markets.push(market),
240 Err(e) => {
241 warn!(error = %e, "Failed to parse market");
242 }
243 }
244 }
245
246 let markets = self.base().set_markets(markets, None).await?;
248
249 info!("Loaded {} markets for Bybit", markets.len());
250 Ok(markets)
251 }
252
253 pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Arc<Market>>> {
265 let _loading_guard = self.base().market_loading_lock.lock().await;
268
269 {
271 let cache = self.base().market_cache.read().await;
272 if cache.loaded && !reload {
273 debug!(
274 "Returning cached markets for Bybit ({} markets)",
275 cache.markets.len()
276 );
277 return Ok(cache.markets.clone());
278 }
279 }
280
281 info!("Loading markets for Bybit (reload: {})", reload);
282 let _markets = self.fetch_markets().await?;
283
284 let cache = self.base().market_cache.read().await;
285 Ok(cache.markets.clone())
286 }
287
288 pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
298 let market = self.base().market(symbol).await?;
299
300 let path = self.build_api_path("/market/tickers");
301 let mut params = HashMap::new();
302 params.insert("category".to_string(), self.get_category().to_string());
303 params.insert("symbol".to_string(), market.id.clone());
304
305 let response = self.public_request("GET", &path, Some(¶ms)).await?;
306
307 let result = response
308 .get("result")
309 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
310
311 let list = result
312 .get("list")
313 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
314
315 let tickers = list.as_array().ok_or_else(|| {
316 Error::from(ParseError::invalid_format(
317 "list",
318 "Expected array of tickers",
319 ))
320 })?;
321
322 if tickers.is_empty() {
323 return Err(Error::bad_symbol(format!("No ticker data for {}", symbol)));
324 }
325
326 parser::parse_ticker(&tickers[0], Some(&market))
327 }
328
329 pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
339 let cache = self.base().market_cache.read().await;
340 if !cache.loaded {
341 drop(cache);
342 return Err(Error::exchange(
343 "-1",
344 "Markets not loaded. Call load_markets() first.",
345 ));
346 }
347 drop(cache);
348
349 let path = self.build_api_path("/market/tickers");
350 let mut params = HashMap::new();
351 params.insert("category".to_string(), self.get_category().to_string());
352
353 let response = self.public_request("GET", &path, Some(¶ms)).await?;
354
355 let result = response
356 .get("result")
357 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
358
359 let list = result
360 .get("list")
361 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
362
363 let tickers_array = list.as_array().ok_or_else(|| {
364 Error::from(ParseError::invalid_format(
365 "list",
366 "Expected array of tickers",
367 ))
368 })?;
369
370 let mut tickers = Vec::new();
371 for ticker_data in tickers_array {
372 if let Some(symbol_id) = ticker_data["symbol"].as_str() {
373 let cache = self.base().market_cache.read().await;
374 if let Some(market) = cache.markets_by_id.get(symbol_id) {
375 let market_clone = market.clone();
376 drop(cache);
377
378 match parser::parse_ticker(ticker_data, Some(&market_clone)) {
379 Ok(ticker) => {
380 if let Some(ref syms) = symbols {
381 if syms.contains(&ticker.symbol) {
382 tickers.push(ticker);
383 }
384 } else {
385 tickers.push(ticker);
386 }
387 }
388 Err(e) => {
389 warn!(
390 error = %e,
391 symbol = %symbol_id,
392 "Failed to parse ticker"
393 );
394 }
395 }
396 } else {
397 drop(cache);
398 }
399 }
400 }
401
402 Ok(tickers)
403 }
404
405 pub async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
420 let market = self.base().market(symbol).await?;
421
422 let path = self.build_api_path("/market/orderbook");
423 let mut params = HashMap::new();
424 params.insert("category".to_string(), self.get_category().to_string());
425 params.insert("symbol".to_string(), market.id.clone());
426
427 let actual_limit = limit.map(|l| l.min(500)).unwrap_or(25);
430 params.insert("limit".to_string(), actual_limit.to_string());
431
432 let response = self.public_request("GET", &path, Some(¶ms)).await?;
433
434 let result = response
435 .get("result")
436 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
437
438 parser::parse_orderbook(result, market.symbol.clone())
439 }
440
441 pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
452 let market = self.base().market(symbol).await?;
453
454 let path = self.build_api_path("/market/recent-trade");
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(|l| l.min(1000)).unwrap_or(60);
461 params.insert("limit".to_string(), actual_limit.to_string());
462
463 let response = self.public_request("GET", &path, Some(¶ms)).await?;
464
465 let result = response
466 .get("result")
467 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
468
469 let list = result
470 .get("list")
471 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
472
473 let trades_array = list.as_array().ok_or_else(|| {
474 Error::from(ParseError::invalid_format(
475 "list",
476 "Expected array of trades",
477 ))
478 })?;
479
480 let mut trades = Vec::new();
481 for trade_data in trades_array {
482 match parser::parse_trade(trade_data, Some(&market)) {
483 Ok(trade) => trades.push(trade),
484 Err(e) => {
485 warn!(error = %e, "Failed to parse trade");
486 }
487 }
488 }
489
490 trades.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
492
493 Ok(trades)
494 }
495
496 pub async fn fetch_ohlcv(
509 &self,
510 symbol: &str,
511 timeframe: &str,
512 since: Option<i64>,
513 limit: Option<u32>,
514 ) -> Result<Vec<OHLCV>> {
515 let market = self.base().market(symbol).await?;
516
517 let timeframes = self.timeframes();
519 let bybit_timeframe = timeframes.get(timeframe).ok_or_else(|| {
520 Error::invalid_request(format!("Unsupported timeframe: {}", timeframe))
521 })?;
522
523 let path = self.build_api_path("/market/kline");
524 let mut params = HashMap::new();
525 params.insert("category".to_string(), self.get_category().to_string());
526 params.insert("symbol".to_string(), market.id.clone());
527 params.insert("interval".to_string(), bybit_timeframe.clone());
528
529 let actual_limit = limit.map(|l| l.min(1000)).unwrap_or(200);
531 params.insert("limit".to_string(), actual_limit.to_string());
532
533 if let Some(start_time) = since {
534 params.insert("start".to_string(), start_time.to_string());
535 }
536
537 let response = self.public_request("GET", &path, Some(¶ms)).await?;
538
539 let result = response
540 .get("result")
541 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
542
543 let list = result
544 .get("list")
545 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
546
547 let candles_array = list.as_array().ok_or_else(|| {
548 Error::from(ParseError::invalid_format(
549 "list",
550 "Expected array of candles",
551 ))
552 })?;
553
554 let mut ohlcv = Vec::new();
555 for candle_data in candles_array {
556 match parser::parse_ohlcv(candle_data) {
557 Ok(candle) => ohlcv.push(candle),
558 Err(e) => {
559 warn!(error = %e, "Failed to parse OHLCV");
560 }
561 }
562 }
563
564 ohlcv.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
566
567 Ok(ohlcv)
568 }
569
570 pub async fn fetch_balance(&self) -> Result<Balance> {
584 let path = self.build_api_path("/account/wallet-balance");
585 let mut params = HashMap::new();
586 params.insert(
587 "accountType".to_string(),
588 self.options().account_type.clone(),
589 );
590
591 let response = self
592 .private_request("GET", &path, Some(¶ms), None)
593 .await?;
594
595 let result = response
596 .get("result")
597 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
598
599 parser::parse_balance(result)
600 }
601
602 pub async fn fetch_my_trades(
614 &self,
615 symbol: &str,
616 since: Option<i64>,
617 limit: Option<u32>,
618 ) -> Result<Vec<Trade>> {
619 let market = self.base().market(symbol).await?;
620
621 let path = self.build_api_path("/execution/list");
622 let mut params = HashMap::new();
623 params.insert("category".to_string(), self.get_category().to_string());
624 params.insert("symbol".to_string(), market.id.clone());
625
626 let actual_limit = limit.map(|l| l.min(100)).unwrap_or(50);
628 params.insert("limit".to_string(), actual_limit.to_string());
629
630 if let Some(start_time) = since {
631 params.insert("startTime".to_string(), start_time.to_string());
632 }
633
634 let response = self
635 .private_request("GET", &path, Some(¶ms), None)
636 .await?;
637
638 let result = response
639 .get("result")
640 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
641
642 let list = result
643 .get("list")
644 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
645
646 let trades_array = list.as_array().ok_or_else(|| {
647 Error::from(ParseError::invalid_format(
648 "list",
649 "Expected array of trades",
650 ))
651 })?;
652
653 let mut trades = Vec::new();
654 for trade_data in trades_array {
655 match parser::parse_trade(trade_data, Some(&market)) {
656 Ok(trade) => trades.push(trade),
657 Err(e) => {
658 warn!(error = %e, "Failed to parse my trade");
659 }
660 }
661 }
662
663 Ok(trades)
664 }
665
666 pub async fn create_order(
684 &self,
685 symbol: &str,
686 order_type: OrderType,
687 side: OrderSide,
688 amount: f64,
689 price: Option<f64>,
690 ) -> Result<Order> {
691 let market = self.base().market(symbol).await?;
692
693 let path = self.build_api_path("/order/create");
694
695 let mut map = serde_json::Map::new();
697 map.insert(
698 "category".to_string(),
699 serde_json::Value::String(self.get_category().to_string()),
700 );
701 map.insert(
702 "symbol".to_string(),
703 serde_json::Value::String(market.id.clone()),
704 );
705 map.insert(
706 "side".to_string(),
707 serde_json::Value::String(match side {
708 OrderSide::Buy => "Buy".to_string(),
709 OrderSide::Sell => "Sell".to_string(),
710 }),
711 );
712 map.insert(
713 "orderType".to_string(),
714 serde_json::Value::String(match order_type {
715 OrderType::Market => "Market".to_string(),
716 OrderType::Limit => "Limit".to_string(),
717 OrderType::LimitMaker => "Limit".to_string(),
718 _ => "Limit".to_string(),
719 }),
720 );
721 map.insert(
722 "qty".to_string(),
723 serde_json::Value::String(amount.to_string()),
724 );
725
726 if let Some(p) = price {
728 if order_type == OrderType::Limit || order_type == OrderType::LimitMaker {
729 map.insert(
730 "price".to_string(),
731 serde_json::Value::String(p.to_string()),
732 );
733 }
734 }
735
736 if order_type == OrderType::LimitMaker {
738 map.insert(
739 "timeInForce".to_string(),
740 serde_json::Value::String("PostOnly".to_string()),
741 );
742 }
743
744 let body = serde_json::Value::Object(map);
745
746 let response = self
747 .private_request("POST", &path, None, Some(&body))
748 .await?;
749
750 let result = response
751 .get("result")
752 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
753
754 parser::parse_order(result, Some(&market))
755 }
756
757 pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
768 let market = self.base().market(symbol).await?;
769
770 let path = self.build_api_path("/order/cancel");
771
772 let mut map = serde_json::Map::new();
773 map.insert(
774 "category".to_string(),
775 serde_json::Value::String(self.get_category().to_string()),
776 );
777 map.insert(
778 "symbol".to_string(),
779 serde_json::Value::String(market.id.clone()),
780 );
781 map.insert(
782 "orderId".to_string(),
783 serde_json::Value::String(id.to_string()),
784 );
785 let body = serde_json::Value::Object(map);
786
787 let response = self
788 .private_request("POST", &path, None, Some(&body))
789 .await?;
790
791 let result = response
792 .get("result")
793 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
794
795 parser::parse_order(result, Some(&market))
796 }
797
798 pub async fn fetch_order(&self, id: &str, symbol: &str) -> Result<Order> {
809 let market = self.base().market(symbol).await?;
810
811 let path = self.build_api_path("/order/realtime");
812 let mut params = HashMap::new();
813 params.insert("category".to_string(), self.get_category().to_string());
814 params.insert("symbol".to_string(), market.id.clone());
815 params.insert("orderId".to_string(), id.to_string());
816
817 let response = self
818 .private_request("GET", &path, Some(¶ms), None)
819 .await?;
820
821 let result = response
822 .get("result")
823 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
824
825 let list = result
826 .get("list")
827 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
828
829 let orders = list.as_array().ok_or_else(|| {
830 Error::from(ParseError::invalid_format(
831 "list",
832 "Expected array of orders",
833 ))
834 })?;
835
836 if orders.is_empty() {
837 return Err(Error::exchange("110008", "Order not found"));
838 }
839
840 parser::parse_order(&orders[0], Some(&market))
841 }
842
843 pub async fn fetch_open_orders(
855 &self,
856 symbol: Option<&str>,
857 since: Option<i64>,
858 limit: Option<u32>,
859 ) -> Result<Vec<Order>> {
860 let path = self.build_api_path("/order/realtime");
861 let mut params = HashMap::new();
862 params.insert("category".to_string(), self.get_category().to_string());
863
864 let market = if let Some(sym) = symbol {
865 let m = self.base().market(sym).await?;
866 params.insert("symbol".to_string(), m.id.clone());
867 Some(m)
868 } else {
869 None
870 };
871
872 let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
874 params.insert("limit".to_string(), actual_limit.to_string());
875
876 if let Some(start_time) = since {
877 params.insert("startTime".to_string(), start_time.to_string());
878 }
879
880 let response = self
881 .private_request("GET", &path, Some(¶ms), None)
882 .await?;
883
884 let result = response
885 .get("result")
886 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
887
888 let list = result
889 .get("list")
890 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
891
892 let orders_array = list.as_array().ok_or_else(|| {
893 Error::from(ParseError::invalid_format(
894 "list",
895 "Expected array of orders",
896 ))
897 })?;
898
899 let mut orders = Vec::new();
900 for order_data in orders_array {
901 match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
902 Ok(order) => orders.push(order),
903 Err(e) => {
904 warn!(error = %e, "Failed to parse open order");
905 }
906 }
907 }
908
909 Ok(orders)
910 }
911
912 pub async fn fetch_closed_orders(
924 &self,
925 symbol: Option<&str>,
926 since: Option<i64>,
927 limit: Option<u32>,
928 ) -> Result<Vec<Order>> {
929 let path = self.build_api_path("/order/history");
930 let mut params = HashMap::new();
931 params.insert("category".to_string(), self.get_category().to_string());
932
933 let market = if let Some(sym) = symbol {
934 let m = self.base().market(sym).await?;
935 params.insert("symbol".to_string(), m.id.clone());
936 Some(m)
937 } else {
938 None
939 };
940
941 let actual_limit = limit.map(|l| l.min(50)).unwrap_or(50);
943 params.insert("limit".to_string(), actual_limit.to_string());
944
945 if let Some(start_time) = since {
946 params.insert("startTime".to_string(), start_time.to_string());
947 }
948
949 let response = self
950 .private_request("GET", &path, Some(¶ms), None)
951 .await?;
952
953 let result = response
954 .get("result")
955 .ok_or_else(|| Error::from(ParseError::missing_field("result")))?;
956
957 let list = result
958 .get("list")
959 .ok_or_else(|| Error::from(ParseError::missing_field("list")))?;
960
961 let orders_array = list.as_array().ok_or_else(|| {
962 Error::from(ParseError::invalid_format(
963 "list",
964 "Expected array of orders",
965 ))
966 })?;
967
968 let mut orders = Vec::new();
969 for order_data in orders_array {
970 match parser::parse_order(order_data, market.as_ref().map(|v| &**v)) {
971 Ok(order) => orders.push(order),
972 Err(e) => {
973 warn!(error = %e, "Failed to parse closed order");
974 }
975 }
976 }
977
978 Ok(orders)
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 fn test_build_api_path() {
988 let bybit = Bybit::builder().build().unwrap();
989 let path = bybit.build_api_path("/market/instruments-info");
990 assert_eq!(path, "/v5/market/instruments-info");
991 }
992
993 #[test]
994 fn test_get_category_spot() {
995 let bybit = Bybit::builder().build().unwrap();
996 let category = bybit.get_category();
997 assert_eq!(category, "spot");
998 }
999
1000 #[test]
1001 fn test_get_category_linear() {
1002 let bybit = Bybit::builder().account_type("LINEAR").build().unwrap();
1003 let category = bybit.get_category();
1004 assert_eq!(category, "linear");
1005 }
1006
1007 #[test]
1008 fn test_get_timestamp() {
1009 let bybit = Bybit::builder().build().unwrap();
1010 let ts = bybit.get_timestamp();
1011
1012 assert!(!ts.is_empty());
1014 let parsed: i64 = ts.parse().unwrap();
1015 assert!(parsed > 0);
1016 }
1017}