1use std::{
21 collections::HashMap,
22 fmt::Debug,
23 num::NonZeroU32,
24 sync::{
25 Arc, LazyLock,
26 atomic::{AtomicBool, Ordering},
27 },
28};
29
30use ahash::{AHashMap, AHashSet};
31use chrono::{DateTime, Utc};
32use dashmap::DashMap;
33use nautilus_core::{
34 consts::NAUTILUS_USER_AGENT, env::get_or_env_var_opt, nanos::UnixNanos,
35 time::get_atomic_clock_realtime,
36};
37use nautilus_model::{
38 data::{Bar, BarType, TradeTick},
39 enums::{OrderSide, OrderType, PositionSideSpecified, TimeInForce},
40 events::account::state::AccountState,
41 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
42 instruments::{Instrument, InstrumentAny},
43 reports::{FillReport, OrderStatusReport, PositionStatusReport},
44 types::{Price, Quantity},
45};
46use nautilus_network::{
47 http::{HttpClient, Method, USER_AGENT},
48 ratelimiter::quota::Quota,
49 retry::{RetryConfig, RetryManager},
50};
51use rust_decimal::Decimal;
52use serde::{Serialize, de::DeserializeOwned};
53use tokio_util::sync::CancellationToken;
54use ustr::Ustr;
55
56use super::{
57 error::BybitHttpError,
58 models::{
59 BybitAccountDetailsResponse, BybitBorrowResponse, BybitFeeRate, BybitFeeRateResponse,
60 BybitInstrumentInverseResponse, BybitInstrumentLinearResponse,
61 BybitInstrumentOptionResponse, BybitInstrumentSpotResponse, BybitKlinesResponse,
62 BybitNoConvertRepayResponse, BybitOpenOrdersResponse, BybitOrderHistoryResponse,
63 BybitPlaceOrderResponse, BybitPositionListResponse, BybitServerTimeResponse,
64 BybitSetLeverageResponse, BybitSetMarginModeResponse, BybitSetTradingStopResponse,
65 BybitSwitchModeResponse, BybitTickerData, BybitTradeHistoryResponse, BybitTradesResponse,
66 BybitWalletBalanceResponse,
67 },
68 query::{
69 BybitAmendOrderParamsBuilder, BybitBatchAmendOrderEntryBuilder,
70 BybitBatchCancelOrderEntryBuilder, BybitBatchCancelOrderParamsBuilder,
71 BybitBatchPlaceOrderEntryBuilder, BybitBorrowParamsBuilder,
72 BybitCancelAllOrdersParamsBuilder, BybitCancelOrderParamsBuilder, BybitFeeRateParams,
73 BybitInstrumentsInfoParams, BybitKlinesParams, BybitKlinesParamsBuilder,
74 BybitNoConvertRepayParamsBuilder, BybitOpenOrdersParamsBuilder,
75 BybitOrderHistoryParamsBuilder, BybitPlaceOrderParamsBuilder, BybitPositionListParams,
76 BybitSetLeverageParamsBuilder, BybitSetMarginModeParamsBuilder, BybitSetTradingStopParams,
77 BybitSwitchModeParamsBuilder, BybitTickersParams, BybitTradeHistoryParams,
78 BybitTradesParams, BybitTradesParamsBuilder, BybitWalletBalanceParams,
79 },
80};
81use crate::{
82 common::{
83 consts::BYBIT_NAUTILUS_BROKER_ID,
84 credential::Credential,
85 enums::{
86 BybitAccountType, BybitEnvironment, BybitMarginMode, BybitOpenOnly, BybitOrderFilter,
87 BybitOrderSide, BybitOrderType, BybitPositionMode, BybitProductType, BybitTimeInForce,
88 },
89 models::{BybitErrorCheck, BybitResponseCheck},
90 parse::{
91 bar_spec_to_bybit_interval, make_bybit_symbol, parse_account_state, parse_fill_report,
92 parse_inverse_instrument, parse_kline_bar, parse_linear_instrument,
93 parse_option_instrument, parse_order_status_report, parse_position_status_report,
94 parse_spot_instrument, parse_trade_tick,
95 },
96 symbol::BybitSymbol,
97 urls::bybit_http_base_url,
98 },
99 http::query::BybitFeeRateParamsBuilder,
100};
101
102const DEFAULT_RECV_WINDOW_MS: u64 = 5_000;
103
104pub static BYBIT_REST_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
109 Quota::per_second(NonZeroU32::new(10).expect("Should be a valid non-zero u32"))
110});
111
112pub static BYBIT_REPAY_QUOTA: LazyLock<Quota> = LazyLock::new(|| {
116 Quota::per_second(NonZeroU32::new(1).expect("Should be a valid non-zero u32"))
117});
118
119const BYBIT_GLOBAL_RATE_KEY: &str = "bybit:global";
120const BYBIT_REPAY_ROUTE_KEY: &str = "bybit:/v5/account/no-convert-repay";
121
122#[cfg_attr(
127 feature = "python",
128 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
129)]
130#[derive(Clone)]
131pub struct BybitRawHttpClient {
132 base_url: String,
133 client: HttpClient,
134 credential: Option<Credential>,
135 recv_window_ms: u64,
136 retry_manager: RetryManager<BybitHttpError>,
137 cancellation_token: CancellationToken,
138}
139
140impl Default for BybitRawHttpClient {
141 fn default() -> Self {
142 Self::new(None, Some(60), None, None, None, None, None)
143 .expect("Failed to create default BybitRawHttpClient")
144 }
145}
146
147impl Debug for BybitRawHttpClient {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct(stringify!(BybitRawHttpClient))
150 .field("base_url", &self.base_url)
151 .field("has_credentials", &self.credential.is_some())
152 .field("recv_window_ms", &self.recv_window_ms)
153 .finish()
154 }
155}
156
157impl BybitRawHttpClient {
158 pub fn cancel_all_requests(&self) {
160 self.cancellation_token.cancel();
161 }
162
163 pub fn cancellation_token(&self) -> &CancellationToken {
165 &self.cancellation_token
166 }
167
168 #[allow(clippy::too_many_arguments)]
174 pub fn new(
175 base_url: Option<String>,
176 timeout_secs: Option<u64>,
177 max_retries: Option<u32>,
178 retry_delay_ms: Option<u64>,
179 retry_delay_max_ms: Option<u64>,
180 recv_window_ms: Option<u64>,
181 proxy_url: Option<String>,
182 ) -> Result<Self, BybitHttpError> {
183 let retry_config = RetryConfig {
184 max_retries: max_retries.unwrap_or(3),
185 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
186 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
187 backoff_factor: 2.0,
188 jitter_ms: 1000,
189 operation_timeout_ms: Some(60_000),
190 immediate_first: false,
191 max_elapsed_ms: Some(180_000),
192 };
193
194 let retry_manager = RetryManager::new(retry_config);
195
196 Ok(Self {
197 base_url: base_url
198 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
199 client: HttpClient::new(
200 Self::default_headers(),
201 vec![],
202 Self::rate_limiter_quotas(),
203 Some(*BYBIT_REST_QUOTA),
204 timeout_secs,
205 proxy_url,
206 )
207 .map_err(|e| {
208 BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
209 })?,
210 credential: None,
211 recv_window_ms: recv_window_ms.unwrap_or(DEFAULT_RECV_WINDOW_MS),
212 retry_manager,
213 cancellation_token: CancellationToken::new(),
214 })
215 }
216
217 #[allow(clippy::too_many_arguments)]
223 pub fn with_credentials(
224 api_key: String,
225 api_secret: String,
226 base_url: Option<String>,
227 timeout_secs: Option<u64>,
228 max_retries: Option<u32>,
229 retry_delay_ms: Option<u64>,
230 retry_delay_max_ms: Option<u64>,
231 recv_window_ms: Option<u64>,
232 proxy_url: Option<String>,
233 ) -> Result<Self, BybitHttpError> {
234 let retry_config = RetryConfig {
235 max_retries: max_retries.unwrap_or(3),
236 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
237 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
238 backoff_factor: 2.0,
239 jitter_ms: 1000,
240 operation_timeout_ms: Some(60_000),
241 immediate_first: false,
242 max_elapsed_ms: Some(180_000),
243 };
244
245 let retry_manager = RetryManager::new(retry_config);
246
247 Ok(Self {
248 base_url: base_url
249 .unwrap_or_else(|| bybit_http_base_url(BybitEnvironment::Mainnet).to_string()),
250 client: HttpClient::new(
251 Self::default_headers(),
252 vec![],
253 Self::rate_limiter_quotas(),
254 Some(*BYBIT_REST_QUOTA),
255 timeout_secs,
256 proxy_url,
257 )
258 .map_err(|e| {
259 BybitHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
260 })?,
261 credential: Some(Credential::new(api_key, api_secret)),
262 recv_window_ms: recv_window_ms.unwrap_or(DEFAULT_RECV_WINDOW_MS),
263 retry_manager,
264 cancellation_token: CancellationToken::new(),
265 })
266 }
267
268 #[allow(clippy::too_many_arguments)]
280 pub fn new_with_env(
281 api_key: Option<String>,
282 api_secret: Option<String>,
283 base_url: Option<String>,
284 demo: bool,
285 testnet: bool,
286 timeout_secs: Option<u64>,
287 max_retries: Option<u32>,
288 retry_delay_ms: Option<u64>,
289 retry_delay_max_ms: Option<u64>,
290 recv_window_ms: Option<u64>,
291 proxy_url: Option<String>,
292 ) -> Result<Self, BybitHttpError> {
293 let (api_key_env, api_secret_env) = if demo {
294 ("BYBIT_DEMO_API_KEY", "BYBIT_DEMO_API_SECRET")
295 } else if testnet {
296 ("BYBIT_TESTNET_API_KEY", "BYBIT_TESTNET_API_SECRET")
297 } else {
298 ("BYBIT_API_KEY", "BYBIT_API_SECRET")
299 };
300
301 let key = get_or_env_var_opt(api_key, api_key_env);
302 let secret = get_or_env_var_opt(api_secret, api_secret_env);
303
304 if let (Some(k), Some(s)) = (key, secret) {
305 Self::with_credentials(
306 k,
307 s,
308 base_url,
309 timeout_secs,
310 max_retries,
311 retry_delay_ms,
312 retry_delay_max_ms,
313 recv_window_ms,
314 proxy_url,
315 )
316 } else {
317 Self::new(
318 base_url,
319 timeout_secs,
320 max_retries,
321 retry_delay_ms,
322 retry_delay_max_ms,
323 recv_window_ms,
324 proxy_url,
325 )
326 }
327 }
328
329 fn default_headers() -> HashMap<String, String> {
330 HashMap::from([
331 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
332 ("Referer".to_string(), BYBIT_NAUTILUS_BROKER_ID.to_string()),
333 ])
334 }
335
336 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
337 vec![
338 (BYBIT_GLOBAL_RATE_KEY.to_string(), *BYBIT_REST_QUOTA),
339 (BYBIT_REPAY_ROUTE_KEY.to_string(), *BYBIT_REPAY_QUOTA),
340 ]
341 }
342
343 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
344 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
345 let route = format!("bybit:{normalized}");
346
347 vec![BYBIT_GLOBAL_RATE_KEY.to_string(), route]
348 }
349
350 fn sign_request(
351 &self,
352 timestamp: &str,
353 params: Option<&str>,
354 ) -> Result<HashMap<String, String>, BybitHttpError> {
355 let credential = self
356 .credential
357 .as_ref()
358 .ok_or(BybitHttpError::MissingCredentials)?;
359
360 let signature = credential.sign_with_payload(timestamp, self.recv_window_ms, params);
361
362 let mut headers = HashMap::new();
363 headers.insert(
364 "X-BAPI-API-KEY".to_string(),
365 credential.api_key().to_string(),
366 );
367 headers.insert("X-BAPI-TIMESTAMP".to_string(), timestamp.to_string());
368 headers.insert("X-BAPI-SIGN".to_string(), signature);
369 headers.insert(
370 "X-BAPI-RECV-WINDOW".to_string(),
371 self.recv_window_ms.to_string(),
372 );
373
374 Ok(headers)
375 }
376
377 async fn send_request<T: DeserializeOwned + BybitResponseCheck, P: Serialize>(
378 &self,
379 method: Method,
380 endpoint: &str,
381 params: Option<&P>,
382 body: Option<Vec<u8>>,
383 authenticate: bool,
384 ) -> Result<T, BybitHttpError> {
385 let endpoint = endpoint.to_string();
386 let url = format!("{}{endpoint}", self.base_url);
387 let method_clone = method.clone();
388 let body_clone = body.clone();
389
390 let params_str = if method == Method::GET {
392 params
393 .map(serde_urlencoded::to_string)
394 .transpose()
395 .map_err(|e| {
396 BybitHttpError::JsonError(format!("Failed to serialize params: {e}"))
397 })?
398 } else {
399 None
400 };
401
402 let operation = || {
403 let url = url.clone();
404 let method = method_clone.clone();
405 let body = body_clone.clone();
406 let endpoint = endpoint.clone();
407 let params_str = params_str.clone();
408
409 async move {
410 let mut headers = Self::default_headers();
411
412 if authenticate {
413 let timestamp = get_atomic_clock_realtime().get_time_ms().to_string();
414
415 let sign_payload = if method == Method::GET {
416 params_str.as_deref()
417 } else {
418 body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
419 };
420
421 let auth_headers = self.sign_request(×tamp, sign_payload)?;
422 headers.extend(auth_headers);
423 }
424
425 if method == Method::POST || method == Method::PUT {
426 headers.insert("Content-Type".to_string(), "application/json".to_string());
427 }
428
429 let full_url = if let Some(ref query) = params_str {
430 if query.is_empty() {
431 url
432 } else {
433 format!("{url}?{query}")
434 }
435 } else {
436 url
437 };
438
439 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
440
441 let response = self
442 .client
443 .request(
444 method,
445 full_url,
446 None,
447 Some(headers),
448 body,
449 None,
450 Some(rate_limit_keys),
451 )
452 .await?;
453
454 if response.status.as_u16() >= 400 {
455 let body = String::from_utf8_lossy(&response.body).to_string();
456 return Err(BybitHttpError::UnexpectedStatus {
457 status: response.status.as_u16(),
458 body,
459 });
460 }
461
462 match serde_json::from_slice::<T>(&response.body) {
464 Ok(result) => {
465 if result.ret_code() != 0 {
467 return Err(BybitHttpError::BybitError {
468 error_code: result.ret_code() as i32,
469 message: result.ret_msg().to_string(),
470 });
471 }
472 Ok(result)
473 }
474 Err(json_err) => {
475 if let Ok(error_check) =
478 serde_json::from_slice::<BybitErrorCheck>(&response.body)
479 && error_check.ret_code != 0
480 {
481 return Err(BybitHttpError::BybitError {
482 error_code: error_check.ret_code as i32,
483 message: error_check.ret_msg,
484 });
485 }
486 Err(json_err.into())
488 }
489 }
490 }
491 };
492
493 let should_retry = |error: &BybitHttpError| -> bool {
494 match error {
495 BybitHttpError::NetworkError(_) => true,
496 BybitHttpError::UnexpectedStatus { status, .. } => *status >= 500,
497 _ => false,
498 }
499 };
500
501 let create_error = |msg: String| -> BybitHttpError {
502 if msg == "canceled" {
503 BybitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
504 } else {
505 BybitHttpError::NetworkError(msg)
506 }
507 };
508
509 self.retry_manager
510 .execute_with_retry_with_cancel(
511 endpoint.as_str(),
512 operation,
513 should_retry,
514 create_error,
515 &self.cancellation_token,
516 )
517 .await
518 }
519
520 #[cfg(test)]
521 fn build_path<S: Serialize>(base: &str, params: &S) -> Result<String, BybitHttpError> {
522 let query = serde_urlencoded::to_string(params)
523 .map_err(|e| BybitHttpError::JsonError(e.to_string()))?;
524 if query.is_empty() {
525 Ok(base.to_owned())
526 } else {
527 Ok(format!("{base}?{query}"))
528 }
529 }
530
531 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
541 self.send_request::<_, ()>(Method::GET, "/v5/market/time", None, None, false)
542 .await
543 }
544
545 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
555 &self,
556 params: &BybitInstrumentsInfoParams,
557 ) -> Result<T, BybitHttpError> {
558 self.send_request(
559 Method::GET,
560 "/v5/market/instruments-info",
561 Some(params),
562 None,
563 false,
564 )
565 .await
566 }
567
568 pub async fn get_instruments_spot(
578 &self,
579 params: &BybitInstrumentsInfoParams,
580 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
581 self.get_instruments(params).await
582 }
583
584 pub async fn get_instruments_linear(
594 &self,
595 params: &BybitInstrumentsInfoParams,
596 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
597 self.get_instruments(params).await
598 }
599
600 pub async fn get_instruments_inverse(
610 &self,
611 params: &BybitInstrumentsInfoParams,
612 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
613 self.get_instruments(params).await
614 }
615
616 pub async fn get_instruments_option(
626 &self,
627 params: &BybitInstrumentsInfoParams,
628 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
629 self.get_instruments(params).await
630 }
631
632 pub async fn get_klines(
642 &self,
643 params: &BybitKlinesParams,
644 ) -> Result<BybitKlinesResponse, BybitHttpError> {
645 self.send_request(Method::GET, "/v5/market/kline", Some(params), None, false)
646 .await
647 }
648
649 pub async fn get_recent_trades(
659 &self,
660 params: &BybitTradesParams,
661 ) -> Result<BybitTradesResponse, BybitHttpError> {
662 self.send_request(
663 Method::GET,
664 "/v5/market/recent-trade",
665 Some(params),
666 None,
667 false,
668 )
669 .await
670 }
671
672 #[allow(clippy::too_many_arguments)]
686 pub async fn get_open_orders(
687 &self,
688 category: BybitProductType,
689 symbol: Option<String>,
690 base_coin: Option<String>,
691 settle_coin: Option<String>,
692 order_id: Option<String>,
693 order_link_id: Option<String>,
694 open_only: Option<BybitOpenOnly>,
695 order_filter: Option<BybitOrderFilter>,
696 limit: Option<u32>,
697 cursor: Option<String>,
698 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
699 let mut builder = BybitOpenOrdersParamsBuilder::default();
700 builder.category(category);
701
702 if let Some(s) = symbol {
703 builder.symbol(s);
704 }
705 if let Some(bc) = base_coin {
706 builder.base_coin(bc);
707 }
708 if let Some(sc) = settle_coin {
709 builder.settle_coin(sc);
710 }
711 if let Some(oi) = order_id {
712 builder.order_id(oi);
713 }
714 if let Some(ol) = order_link_id {
715 builder.order_link_id(ol);
716 }
717 if let Some(oo) = open_only {
718 builder.open_only(oo);
719 }
720 if let Some(of) = order_filter {
721 builder.order_filter(of);
722 }
723 if let Some(l) = limit {
724 builder.limit(l);
725 }
726 if let Some(c) = cursor {
727 builder.cursor(c);
728 }
729
730 let params = builder
731 .build()
732 .expect("Failed to build BybitOpenOrdersParams");
733
734 self.send_request(Method::GET, "/v5/order/realtime", Some(¶ms), None, true)
735 .await
736 }
737
738 pub async fn place_order(
748 &self,
749 request: &serde_json::Value,
750 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
751 let body = serde_json::to_vec(request)?;
752 self.send_request::<_, ()>(Method::POST, "/v5/order/create", None, Some(body), true)
753 .await
754 }
755
756 pub async fn get_wallet_balance(
766 &self,
767 params: &BybitWalletBalanceParams,
768 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
769 self.send_request(
770 Method::GET,
771 "/v5/account/wallet-balance",
772 Some(params),
773 None,
774 true,
775 )
776 .await
777 }
778
779 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
789 self.send_request::<_, ()>(Method::GET, "/v5/user/query-api", None, None, true)
790 .await
791 }
792
793 pub async fn get_fee_rate(
803 &self,
804 params: &BybitFeeRateParams,
805 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
806 self.send_request(
807 Method::GET,
808 "/v5/account/fee-rate",
809 Some(params),
810 None,
811 true,
812 )
813 .await
814 }
815
816 pub async fn set_margin_mode(
833 &self,
834 margin_mode: BybitMarginMode,
835 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
836 let params = BybitSetMarginModeParamsBuilder::default()
837 .set_margin_mode(margin_mode)
838 .build()
839 .expect("Failed to build BybitSetMarginModeParams");
840
841 let body = serde_json::to_vec(¶ms)?;
842 self.send_request::<_, ()>(
843 Method::POST,
844 "/v5/account/set-margin-mode",
845 None,
846 Some(body),
847 true,
848 )
849 .await
850 }
851
852 pub async fn set_leverage(
869 &self,
870 product_type: BybitProductType,
871 symbol: &str,
872 buy_leverage: &str,
873 sell_leverage: &str,
874 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
875 let params = BybitSetLeverageParamsBuilder::default()
876 .category(product_type)
877 .symbol(symbol.to_string())
878 .buy_leverage(buy_leverage.to_string())
879 .sell_leverage(sell_leverage.to_string())
880 .build()
881 .expect("Failed to build BybitSetLeverageParams");
882
883 let body = serde_json::to_vec(¶ms)?;
884 self.send_request::<_, ()>(
885 Method::POST,
886 "/v5/position/set-leverage",
887 None,
888 Some(body),
889 true,
890 )
891 .await
892 }
893
894 pub async fn switch_mode(
911 &self,
912 product_type: BybitProductType,
913 mode: BybitPositionMode,
914 symbol: Option<String>,
915 coin: Option<String>,
916 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
917 let mut builder = BybitSwitchModeParamsBuilder::default();
918 builder.category(product_type);
919 builder.mode(mode);
920
921 if let Some(s) = symbol {
922 builder.symbol(s);
923 }
924 if let Some(c) = coin {
925 builder.coin(c);
926 }
927
928 let params = builder
929 .build()
930 .expect("Failed to build BybitSwitchModeParams");
931
932 let body = serde_json::to_vec(¶ms)?;
933 self.send_request::<_, ()>(
934 Method::POST,
935 "/v5/position/switch-mode",
936 None,
937 Some(body),
938 true,
939 )
940 .await
941 }
942
943 pub async fn set_trading_stop(
956 &self,
957 params: &BybitSetTradingStopParams,
958 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
959 let body = serde_json::to_vec(params)?;
960 self.send_request::<_, ()>(
961 Method::POST,
962 "/v5/position/trading-stop",
963 None,
964 Some(body),
965 true,
966 )
967 .await
968 }
969
970 pub async fn borrow(
987 &self,
988 coin: &str,
989 amount: &str,
990 ) -> Result<BybitBorrowResponse, BybitHttpError> {
991 let params = BybitBorrowParamsBuilder::default()
992 .coin(coin.to_string())
993 .amount(amount.to_string())
994 .build()
995 .expect("Failed to build BybitBorrowParams");
996
997 let body = serde_json::to_vec(¶ms)?;
998 self.send_request::<_, ()>(Method::POST, "/v5/account/borrow", None, Some(body), true)
999 .await
1000 }
1001
1002 pub async fn no_convert_repay(
1020 &self,
1021 coin: &str,
1022 amount: Option<&str>,
1023 ) -> Result<BybitNoConvertRepayResponse, BybitHttpError> {
1024 let mut builder = BybitNoConvertRepayParamsBuilder::default();
1025 builder.coin(coin.to_string());
1026
1027 if let Some(amt) = amount {
1028 builder.amount(amt.to_string());
1029 }
1030
1031 let params = builder
1032 .build()
1033 .expect("Failed to build BybitNoConvertRepayParams");
1034
1035 if let Ok(params_json) = serde_json::to_string(¶ms) {
1037 log::debug!("Repay request params: {params_json}");
1038 }
1039
1040 let body = serde_json::to_vec(¶ms)?;
1041 let result = self
1042 .send_request::<_, ()>(
1043 Method::POST,
1044 "/v5/account/no-convert-repay",
1045 None,
1046 Some(body),
1047 true,
1048 )
1049 .await;
1050
1051 if let Err(ref e) = result
1053 && let Ok(params_json) = serde_json::to_string(¶ms)
1054 {
1055 log::error!("Repay request failed with params {params_json}: {e}");
1056 }
1057
1058 result
1059 }
1060
1061 pub async fn get_tickers<T: DeserializeOwned + BybitResponseCheck>(
1071 &self,
1072 params: &BybitTickersParams,
1073 ) -> Result<T, BybitHttpError> {
1074 self.send_request(Method::GET, "/v5/market/tickers", Some(params), None, false)
1075 .await
1076 }
1077
1078 pub async fn get_trade_history(
1088 &self,
1089 params: &BybitTradeHistoryParams,
1090 ) -> Result<BybitTradeHistoryResponse, BybitHttpError> {
1091 self.send_request(Method::GET, "/v5/execution/list", Some(params), None, true)
1092 .await
1093 }
1094
1095 pub async fn get_positions(
1108 &self,
1109 params: &BybitPositionListParams,
1110 ) -> Result<BybitPositionListResponse, BybitHttpError> {
1111 self.send_request(Method::GET, "/v5/position/list", Some(params), None, true)
1112 .await
1113 }
1114
1115 #[must_use]
1117 pub fn base_url(&self) -> &str {
1118 &self.base_url
1119 }
1120
1121 #[must_use]
1123 pub fn recv_window_ms(&self) -> u64 {
1124 self.recv_window_ms
1125 }
1126
1127 #[must_use]
1129 pub fn credential(&self) -> Option<&Credential> {
1130 self.credential.as_ref()
1131 }
1132}
1133
1134#[cfg_attr(
1136 feature = "python",
1137 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit")
1138)]
1139pub struct BybitHttpClient {
1144 pub(crate) inner: Arc<BybitRawHttpClient>,
1145 pub(crate) instruments_cache: Arc<DashMap<Ustr, InstrumentAny>>,
1146 cache_initialized: Arc<AtomicBool>,
1147 use_spot_position_reports: Arc<AtomicBool>,
1148}
1149
1150impl Clone for BybitHttpClient {
1151 fn clone(&self) -> Self {
1152 Self {
1153 inner: self.inner.clone(),
1154 instruments_cache: self.instruments_cache.clone(),
1155 cache_initialized: self.cache_initialized.clone(),
1156 use_spot_position_reports: self.use_spot_position_reports.clone(),
1157 }
1158 }
1159}
1160
1161impl Default for BybitHttpClient {
1162 fn default() -> Self {
1163 Self::new(None, Some(60), None, None, None, None, None)
1164 .expect("Failed to create default BybitHttpClient")
1165 }
1166}
1167
1168impl Debug for BybitHttpClient {
1169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1170 f.debug_struct(stringify!(BybitHttpClient))
1171 .field("inner", &self.inner)
1172 .finish()
1173 }
1174}
1175
1176impl BybitHttpClient {
1177 #[allow(clippy::too_many_arguments)]
1183 pub fn new(
1184 base_url: Option<String>,
1185 timeout_secs: Option<u64>,
1186 max_retries: Option<u32>,
1187 retry_delay_ms: Option<u64>,
1188 retry_delay_max_ms: Option<u64>,
1189 recv_window_ms: Option<u64>,
1190 proxy_url: Option<String>,
1191 ) -> Result<Self, BybitHttpError> {
1192 Ok(Self {
1193 inner: Arc::new(BybitRawHttpClient::new(
1194 base_url,
1195 timeout_secs,
1196 max_retries,
1197 retry_delay_ms,
1198 retry_delay_max_ms,
1199 recv_window_ms,
1200 proxy_url,
1201 )?),
1202 instruments_cache: Arc::new(DashMap::new()),
1203 cache_initialized: Arc::new(AtomicBool::new(false)),
1204 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1205 })
1206 }
1207
1208 #[allow(clippy::too_many_arguments)]
1214 pub fn with_credentials(
1215 api_key: String,
1216 api_secret: String,
1217 base_url: Option<String>,
1218 timeout_secs: Option<u64>,
1219 max_retries: Option<u32>,
1220 retry_delay_ms: Option<u64>,
1221 retry_delay_max_ms: Option<u64>,
1222 recv_window_ms: Option<u64>,
1223 proxy_url: Option<String>,
1224 ) -> Result<Self, BybitHttpError> {
1225 Ok(Self {
1226 inner: Arc::new(BybitRawHttpClient::with_credentials(
1227 api_key,
1228 api_secret,
1229 base_url,
1230 timeout_secs,
1231 max_retries,
1232 retry_delay_ms,
1233 retry_delay_max_ms,
1234 recv_window_ms,
1235 proxy_url,
1236 )?),
1237 instruments_cache: Arc::new(DashMap::new()),
1238 cache_initialized: Arc::new(AtomicBool::new(false)),
1239 use_spot_position_reports: Arc::new(AtomicBool::new(false)),
1240 })
1241 }
1242
1243 #[allow(clippy::too_many_arguments)]
1256 pub fn new_with_env(
1257 api_key: Option<String>,
1258 api_secret: Option<String>,
1259 base_url: Option<String>,
1260 demo: bool,
1261 testnet: bool,
1262 timeout_secs: Option<u64>,
1263 max_retries: Option<u32>,
1264 retry_delay_ms: Option<u64>,
1265 retry_delay_max_ms: Option<u64>,
1266 recv_window_ms: Option<u64>,
1267 proxy_url: Option<String>,
1268 ) -> Result<Self, BybitHttpError> {
1269 let (api_key_env, api_secret_env) = if demo {
1270 ("BYBIT_DEMO_API_KEY", "BYBIT_DEMO_API_SECRET")
1271 } else if testnet {
1272 ("BYBIT_TESTNET_API_KEY", "BYBIT_TESTNET_API_SECRET")
1273 } else {
1274 ("BYBIT_API_KEY", "BYBIT_API_SECRET")
1275 };
1276
1277 let key = get_or_env_var_opt(api_key, api_key_env);
1278 let secret = get_or_env_var_opt(api_secret, api_secret_env);
1279
1280 match (key, secret) {
1281 (Some(k), Some(s)) => Self::with_credentials(
1282 k,
1283 s,
1284 base_url,
1285 timeout_secs,
1286 max_retries,
1287 retry_delay_ms,
1288 retry_delay_max_ms,
1289 recv_window_ms,
1290 proxy_url,
1291 ),
1292 _ => Self::new(
1293 base_url,
1294 timeout_secs,
1295 max_retries,
1296 retry_delay_ms,
1297 retry_delay_max_ms,
1298 recv_window_ms,
1299 proxy_url,
1300 ),
1301 }
1302 }
1303
1304 #[must_use]
1305 pub fn base_url(&self) -> &str {
1306 self.inner.base_url()
1307 }
1308
1309 #[must_use]
1310 pub fn recv_window_ms(&self) -> u64 {
1311 self.inner.recv_window_ms()
1312 }
1313
1314 #[must_use]
1315 pub fn credential(&self) -> Option<&Credential> {
1316 self.inner.credential()
1317 }
1318
1319 pub fn set_use_spot_position_reports(&self, use_spot_position_reports: bool) {
1320 self.use_spot_position_reports
1321 .store(use_spot_position_reports, Ordering::Relaxed);
1322 }
1323
1324 pub fn cancel_all_requests(&self) {
1325 self.inner.cancel_all_requests();
1326 }
1327
1328 pub fn cancellation_token(&self) -> &CancellationToken {
1329 self.inner.cancellation_token()
1330 }
1331
1332 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1334 self.instruments_cache
1335 .insert(instrument.symbol().inner(), instrument);
1336 self.cache_initialized.store(true, Ordering::Release);
1337 }
1338
1339 pub fn cache_instruments(&self, instruments: Vec<InstrumentAny>) {
1341 for instrument in instruments {
1342 self.instruments_cache
1343 .insert(instrument.symbol().inner(), instrument);
1344 }
1345 self.cache_initialized.store(true, Ordering::Release);
1346 }
1347
1348 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1349 self.instruments_cache
1350 .get(symbol)
1351 .map(|entry| entry.value().clone())
1352 }
1353
1354 fn instrument_from_cache(&self, symbol: &Symbol) -> anyhow::Result<InstrumentAny> {
1355 self.get_instrument(&symbol.inner()).ok_or_else(|| {
1356 anyhow::anyhow!(
1357 "Instrument {symbol} not found in cache, ensure instruments loaded first"
1358 )
1359 })
1360 }
1361
1362 #[must_use]
1363 fn generate_ts_init(&self) -> UnixNanos {
1364 get_atomic_clock_realtime().get_time_ns()
1365 }
1366
1367 pub async fn get_server_time(&self) -> Result<BybitServerTimeResponse, BybitHttpError> {
1379 self.inner.get_server_time().await
1380 }
1381
1382 pub async fn get_instruments<T: DeserializeOwned + BybitResponseCheck>(
1394 &self,
1395 params: &BybitInstrumentsInfoParams,
1396 ) -> Result<T, BybitHttpError> {
1397 self.inner.get_instruments(params).await
1398 }
1399
1400 pub async fn get_instruments_spot(
1412 &self,
1413 params: &BybitInstrumentsInfoParams,
1414 ) -> Result<BybitInstrumentSpotResponse, BybitHttpError> {
1415 self.inner.get_instruments_spot(params).await
1416 }
1417
1418 pub async fn get_instruments_linear(
1430 &self,
1431 params: &BybitInstrumentsInfoParams,
1432 ) -> Result<BybitInstrumentLinearResponse, BybitHttpError> {
1433 self.inner.get_instruments_linear(params).await
1434 }
1435
1436 pub async fn get_instruments_inverse(
1448 &self,
1449 params: &BybitInstrumentsInfoParams,
1450 ) -> Result<BybitInstrumentInverseResponse, BybitHttpError> {
1451 self.inner.get_instruments_inverse(params).await
1452 }
1453
1454 pub async fn get_instruments_option(
1466 &self,
1467 params: &BybitInstrumentsInfoParams,
1468 ) -> Result<BybitInstrumentOptionResponse, BybitHttpError> {
1469 self.inner.get_instruments_option(params).await
1470 }
1471
1472 pub async fn get_klines(
1484 &self,
1485 params: &BybitKlinesParams,
1486 ) -> Result<BybitKlinesResponse, BybitHttpError> {
1487 self.inner.get_klines(params).await
1488 }
1489
1490 pub async fn get_recent_trades(
1502 &self,
1503 params: &BybitTradesParams,
1504 ) -> Result<BybitTradesResponse, BybitHttpError> {
1505 self.inner.get_recent_trades(params).await
1506 }
1507
1508 #[allow(clippy::too_many_arguments)]
1520 pub async fn get_open_orders(
1521 &self,
1522 category: BybitProductType,
1523 symbol: Option<String>,
1524 base_coin: Option<String>,
1525 settle_coin: Option<String>,
1526 order_id: Option<String>,
1527 order_link_id: Option<String>,
1528 open_only: Option<BybitOpenOnly>,
1529 order_filter: Option<BybitOrderFilter>,
1530 limit: Option<u32>,
1531 cursor: Option<String>,
1532 ) -> Result<BybitOpenOrdersResponse, BybitHttpError> {
1533 self.inner
1534 .get_open_orders(
1535 category,
1536 symbol,
1537 base_coin,
1538 settle_coin,
1539 order_id,
1540 order_link_id,
1541 open_only,
1542 order_filter,
1543 limit,
1544 cursor,
1545 )
1546 .await
1547 }
1548
1549 pub async fn place_order(
1561 &self,
1562 request: &serde_json::Value,
1563 ) -> Result<BybitPlaceOrderResponse, BybitHttpError> {
1564 self.inner.place_order(request).await
1565 }
1566
1567 pub async fn get_wallet_balance(
1579 &self,
1580 params: &BybitWalletBalanceParams,
1581 ) -> Result<BybitWalletBalanceResponse, BybitHttpError> {
1582 self.inner.get_wallet_balance(params).await
1583 }
1584
1585 pub async fn get_account_details(&self) -> Result<BybitAccountDetailsResponse, BybitHttpError> {
1597 self.inner.get_account_details().await
1598 }
1599
1600 pub async fn get_positions(
1613 &self,
1614 params: &BybitPositionListParams,
1615 ) -> Result<BybitPositionListResponse, BybitHttpError> {
1616 self.inner.get_positions(params).await
1617 }
1618
1619 pub async fn get_fee_rate(
1632 &self,
1633 params: &BybitFeeRateParams,
1634 ) -> Result<BybitFeeRateResponse, BybitHttpError> {
1635 self.inner.get_fee_rate(params).await
1636 }
1637
1638 pub async fn set_margin_mode(
1651 &self,
1652 margin_mode: BybitMarginMode,
1653 ) -> Result<BybitSetMarginModeResponse, BybitHttpError> {
1654 self.inner.set_margin_mode(margin_mode).await
1655 }
1656
1657 pub async fn set_leverage(
1670 &self,
1671 product_type: BybitProductType,
1672 symbol: &str,
1673 buy_leverage: &str,
1674 sell_leverage: &str,
1675 ) -> Result<BybitSetLeverageResponse, BybitHttpError> {
1676 self.inner
1677 .set_leverage(product_type, symbol, buy_leverage, sell_leverage)
1678 .await
1679 }
1680
1681 pub async fn switch_mode(
1694 &self,
1695 product_type: BybitProductType,
1696 mode: BybitPositionMode,
1697 symbol: Option<String>,
1698 coin: Option<String>,
1699 ) -> Result<BybitSwitchModeResponse, BybitHttpError> {
1700 self.inner
1701 .switch_mode(product_type, mode, symbol, coin)
1702 .await
1703 }
1704
1705 pub async fn set_trading_stop(
1718 &self,
1719 params: &BybitSetTradingStopParams,
1720 ) -> Result<BybitSetTradingStopResponse, BybitHttpError> {
1721 self.inner.set_trading_stop(params).await
1722 }
1723
1724 pub async fn get_spot_borrow_amount(&self, coin: &str) -> anyhow::Result<Decimal> {
1739 let params = BybitWalletBalanceParams {
1740 account_type: BybitAccountType::Unified,
1741 coin: Some(coin.to_string()),
1742 };
1743
1744 let response = self.inner.get_wallet_balance(¶ms).await?;
1745
1746 let borrow_amount = response
1747 .result
1748 .list
1749 .first()
1750 .and_then(|wallet| wallet.coin.iter().find(|c| c.coin.as_str() == coin))
1751 .map_or(Decimal::ZERO, |balance| balance.spot_borrow);
1752
1753 Ok(borrow_amount)
1754 }
1755
1756 pub async fn borrow_spot(
1772 &self,
1773 coin: &str,
1774 amount: Quantity,
1775 ) -> anyhow::Result<BybitBorrowResponse> {
1776 let amount_str = amount.to_string();
1777 self.inner
1778 .borrow(coin, &amount_str)
1779 .await
1780 .map_err(|e| anyhow::anyhow!("Failed to borrow {amount} {coin}: {e}"))
1781 }
1782
1783 pub async fn repay_spot_borrow(
1800 &self,
1801 coin: &str,
1802 amount: Option<Quantity>,
1803 ) -> anyhow::Result<BybitNoConvertRepayResponse> {
1804 let amount_str = amount.as_ref().map(|q| q.to_string());
1805 self.inner
1806 .no_convert_repay(coin, amount_str.as_deref())
1807 .await
1808 .map_err(|e| anyhow::anyhow!("Failed to repay spot borrow for {coin}: {e}"))
1809 }
1810
1811 async fn generate_spot_position_reports_from_wallet(
1819 &self,
1820 account_id: AccountId,
1821 instrument_id: Option<InstrumentId>,
1822 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1823 let params = BybitWalletBalanceParams {
1824 account_type: BybitAccountType::Unified,
1825 coin: None,
1826 };
1827
1828 let response = self.inner.get_wallet_balance(¶ms).await?;
1829 let ts_init = self.generate_ts_init();
1830
1831 let mut wallet_by_coin: HashMap<Ustr, Decimal> = HashMap::new();
1832
1833 for wallet in &response.result.list {
1834 for coin_balance in &wallet.coin {
1835 let balance = coin_balance.wallet_balance - coin_balance.spot_borrow;
1836 *wallet_by_coin
1837 .entry(coin_balance.coin)
1838 .or_insert(Decimal::ZERO) += balance;
1839 }
1840 }
1841
1842 let mut reports = Vec::new();
1843
1844 if let Some(instrument_id) = instrument_id {
1845 if let Some(instrument) = self.instruments_cache.get(&instrument_id.symbol.inner()) {
1846 let base_currency = instrument
1847 .base_currency()
1848 .expect("SPOT instrument should have base currency");
1849 let coin = base_currency.code;
1850 let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
1851
1852 let side = if wallet_balance > Decimal::ZERO {
1853 PositionSideSpecified::Long
1854 } else if wallet_balance < Decimal::ZERO {
1855 PositionSideSpecified::Short
1856 } else {
1857 PositionSideSpecified::Flat
1858 };
1859
1860 let abs_balance = wallet_balance.abs();
1861 let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
1862
1863 let report = PositionStatusReport::new(
1864 account_id,
1865 instrument_id,
1866 side,
1867 quantity,
1868 ts_init,
1869 ts_init,
1870 None,
1871 None,
1872 None,
1873 );
1874
1875 reports.push(report);
1876 }
1877 } else {
1878 for entry in self.instruments_cache.iter() {
1880 let symbol = entry.key();
1881 let instrument = entry.value();
1882 if !symbol.as_str().ends_with("-SPOT") {
1884 continue;
1885 }
1886
1887 let base_currency = match instrument.base_currency() {
1888 Some(currency) => currency,
1889 None => continue,
1890 };
1891
1892 let coin = base_currency.code;
1893 let wallet_balance = wallet_by_coin.get(&coin).copied().unwrap_or(Decimal::ZERO);
1894
1895 if wallet_balance.is_zero() {
1896 continue;
1897 }
1898
1899 let side = if wallet_balance > Decimal::ZERO {
1900 PositionSideSpecified::Long
1901 } else if wallet_balance < Decimal::ZERO {
1902 PositionSideSpecified::Short
1903 } else {
1904 PositionSideSpecified::Flat
1905 };
1906
1907 let abs_balance = wallet_balance.abs();
1908 let quantity = Quantity::from_decimal_dp(abs_balance, instrument.size_precision())?;
1909
1910 if quantity.is_zero() {
1911 continue;
1912 }
1913
1914 let report = PositionStatusReport::new(
1915 account_id,
1916 instrument.id(),
1917 side,
1918 quantity,
1919 ts_init,
1920 ts_init,
1921 None,
1922 None,
1923 None,
1924 );
1925
1926 reports.push(report);
1927 }
1928 }
1929
1930 Ok(reports)
1931 }
1932
1933 #[allow(clippy::too_many_arguments)]
1944 pub async fn submit_order(
1945 &self,
1946 account_id: AccountId,
1947 product_type: BybitProductType,
1948 instrument_id: InstrumentId,
1949 client_order_id: ClientOrderId,
1950 order_side: OrderSide,
1951 order_type: OrderType,
1952 quantity: Quantity,
1953 time_in_force: TimeInForce,
1954 price: Option<Price>,
1955 reduce_only: bool,
1956 is_leverage: bool,
1957 ) -> anyhow::Result<OrderStatusReport> {
1958 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
1959 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
1960
1961 let bybit_side = match order_side {
1962 OrderSide::Buy => BybitOrderSide::Buy,
1963 OrderSide::Sell => BybitOrderSide::Sell,
1964 _ => anyhow::bail!("Invalid order side: {order_side:?}"),
1965 };
1966
1967 let bybit_order_type = match order_type {
1968 OrderType::Market => BybitOrderType::Market,
1969 OrderType::Limit => BybitOrderType::Limit,
1970 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
1971 };
1972
1973 let bybit_tif = match time_in_force {
1974 TimeInForce::Gtc => BybitTimeInForce::Gtc,
1975 TimeInForce::Ioc => BybitTimeInForce::Ioc,
1976 TimeInForce::Fok => BybitTimeInForce::Fok,
1977 _ => anyhow::bail!("Unsupported time in force: {time_in_force:?}"),
1978 };
1979
1980 let mut order_entry = BybitBatchPlaceOrderEntryBuilder::default();
1981 order_entry.symbol(bybit_symbol.raw_symbol().to_string());
1982 order_entry.side(bybit_side);
1983 order_entry.order_type(bybit_order_type);
1984 order_entry.qty(quantity.to_string());
1985 order_entry.time_in_force(Some(bybit_tif));
1986 order_entry.order_link_id(client_order_id.to_string());
1987
1988 if let Some(price) = price {
1989 order_entry.price(Some(price.to_string()));
1990 }
1991
1992 if reduce_only {
1993 order_entry.reduce_only(Some(true));
1994 }
1995
1996 let is_leverage_value = if product_type == BybitProductType::Spot {
1998 Some(i32::from(is_leverage))
1999 } else {
2000 None
2001 };
2002 order_entry.is_leverage(is_leverage_value);
2003
2004 let order_entry = order_entry.build().map_err(|e| anyhow::anyhow!(e))?;
2005
2006 let mut params = BybitPlaceOrderParamsBuilder::default();
2007 params.category(product_type);
2008 params.order(order_entry);
2009
2010 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2011
2012 let body = serde_json::to_value(¶ms)?;
2013 let response = self.inner.place_order(&body).await?;
2014
2015 let order_id = response
2016 .result
2017 .order_id
2018 .ok_or_else(|| anyhow::anyhow!("No order_id in response"))?;
2019
2020 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2022 query_params.category(product_type);
2023 query_params.order_id(order_id.as_str().to_string());
2024
2025 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
2026 let order_response: BybitOpenOrdersResponse = self
2027 .inner
2028 .send_request(
2029 Method::GET,
2030 "/v5/order/realtime",
2031 Some(&query_params),
2032 None,
2033 true,
2034 )
2035 .await?;
2036
2037 let order = order_response
2038 .result
2039 .list
2040 .into_iter()
2041 .next()
2042 .ok_or_else(|| anyhow::anyhow!("No order returned after submission"))?;
2043
2044 if order.order_status == crate::common::enums::BybitOrderStatus::Rejected
2047 && (order.cum_exec_qty.as_str() == "0" || order.cum_exec_qty.is_empty())
2048 {
2049 anyhow::bail!("Order rejected: {}", order.reject_reason);
2050 }
2051
2052 let ts_init = self.generate_ts_init();
2053
2054 parse_order_status_report(&order, &instrument, account_id, ts_init)
2055 }
2056
2057 pub async fn cancel_order(
2067 &self,
2068 account_id: AccountId,
2069 product_type: BybitProductType,
2070 instrument_id: InstrumentId,
2071 client_order_id: Option<ClientOrderId>,
2072 venue_order_id: Option<VenueOrderId>,
2073 ) -> anyhow::Result<OrderStatusReport> {
2074 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2075 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2076
2077 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2078 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2079
2080 if let Some(venue_order_id) = venue_order_id {
2081 cancel_entry.order_id(venue_order_id.to_string());
2082 } else if let Some(client_order_id) = client_order_id {
2083 cancel_entry.order_link_id(client_order_id.to_string());
2084 } else {
2085 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2086 }
2087
2088 let cancel_entry = cancel_entry.build().map_err(|e| anyhow::anyhow!(e))?;
2089
2090 let mut params = BybitCancelOrderParamsBuilder::default();
2091 params.category(product_type);
2092 params.order(cancel_entry);
2093
2094 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2095 let body = serde_json::to_vec(¶ms)?;
2096
2097 let response: BybitPlaceOrderResponse = self
2098 .inner
2099 .send_request::<_, ()>(Method::POST, "/v5/order/cancel", None, Some(body), true)
2100 .await?;
2101
2102 let order_id = response
2103 .result
2104 .order_id
2105 .ok_or_else(|| anyhow::anyhow!("No order_id in cancel response"))?;
2106
2107 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2109 query_params.category(product_type);
2110 query_params.order_id(order_id.as_str().to_string());
2111
2112 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
2113 let order_response: BybitOrderHistoryResponse = self
2114 .inner
2115 .send_request(
2116 Method::GET,
2117 "/v5/order/history",
2118 Some(&query_params),
2119 None,
2120 true,
2121 )
2122 .await?;
2123
2124 let order = order_response
2125 .result
2126 .list
2127 .into_iter()
2128 .next()
2129 .ok_or_else(|| anyhow::anyhow!("No order returned in cancel response"))?;
2130
2131 let ts_init = self.generate_ts_init();
2132
2133 parse_order_status_report(&order, &instrument, account_id, ts_init)
2134 }
2135
2136 pub async fn batch_cancel_orders(
2146 &self,
2147 account_id: AccountId,
2148 product_type: BybitProductType,
2149 instrument_ids: Vec<InstrumentId>,
2150 client_order_ids: Vec<Option<ClientOrderId>>,
2151 venue_order_ids: Vec<Option<VenueOrderId>>,
2152 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2153 if instrument_ids.len() != client_order_ids.len()
2154 || instrument_ids.len() != venue_order_ids.len()
2155 {
2156 anyhow::bail!(
2157 "instrument_ids, client_order_ids, and venue_order_ids must have the same length"
2158 );
2159 }
2160
2161 if instrument_ids.is_empty() {
2162 return Ok(Vec::new());
2163 }
2164
2165 if instrument_ids.len() > 20 {
2166 anyhow::bail!("Batch cancel limit is 20 orders per request");
2167 }
2168
2169 let mut cancel_entries = Vec::new();
2170
2171 for ((instrument_id, client_order_id), venue_order_id) in instrument_ids
2172 .iter()
2173 .zip(client_order_ids.iter())
2174 .zip(venue_order_ids.iter())
2175 {
2176 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2177 let mut cancel_entry = BybitBatchCancelOrderEntryBuilder::default();
2178 cancel_entry.symbol(bybit_symbol.raw_symbol().to_string());
2179
2180 if let Some(venue_order_id) = venue_order_id {
2181 cancel_entry.order_id(venue_order_id.to_string());
2182 } else if let Some(client_order_id) = client_order_id {
2183 cancel_entry.order_link_id(client_order_id.to_string());
2184 } else {
2185 anyhow::bail!(
2186 "Either client_order_id or venue_order_id must be provided for each order"
2187 );
2188 }
2189
2190 cancel_entries.push(cancel_entry.build().map_err(|e| anyhow::anyhow!(e))?);
2191 }
2192
2193 let mut params = BybitBatchCancelOrderParamsBuilder::default();
2194 params.category(product_type);
2195 params.request(cancel_entries);
2196
2197 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2198 let body = serde_json::to_vec(¶ms)?;
2199
2200 let _response: BybitPlaceOrderResponse = self
2201 .inner
2202 .send_request::<_, ()>(
2203 Method::POST,
2204 "/v5/order/cancel-batch",
2205 None,
2206 Some(body),
2207 true,
2208 )
2209 .await?;
2210
2211 let mut reports = Vec::new();
2213 for (instrument_id, (client_order_id, venue_order_id)) in instrument_ids
2214 .iter()
2215 .zip(client_order_ids.iter().zip(venue_order_ids.iter()))
2216 {
2217 let Ok(instrument) = self.instrument_from_cache(&instrument_id.symbol) else {
2218 log::debug!(
2219 "Skipping cancelled order report for instrument not in cache: symbol={}",
2220 instrument_id.symbol
2221 );
2222 continue;
2223 };
2224
2225 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2226
2227 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2228 query_params.category(product_type);
2229 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2230
2231 if let Some(venue_order_id) = venue_order_id {
2232 query_params.order_id(venue_order_id.to_string());
2233 } else if let Some(client_order_id) = client_order_id {
2234 query_params.order_link_id(client_order_id.to_string());
2235 }
2236
2237 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
2238 let order_response: BybitOrderHistoryResponse = self
2239 .inner
2240 .send_request(
2241 Method::GET,
2242 "/v5/order/history",
2243 Some(&query_params),
2244 None,
2245 true,
2246 )
2247 .await?;
2248
2249 if let Some(order) = order_response.result.list.into_iter().next() {
2250 let ts_init = self.generate_ts_init();
2251 let report = parse_order_status_report(&order, &instrument, account_id, ts_init)?;
2252 reports.push(report);
2253 }
2254 }
2255
2256 Ok(reports)
2257 }
2258
2259 pub async fn cancel_all_orders(
2268 &self,
2269 account_id: AccountId,
2270 product_type: BybitProductType,
2271 instrument_id: InstrumentId,
2272 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2273 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2274 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2275
2276 let mut params = BybitCancelAllOrdersParamsBuilder::default();
2277 params.category(product_type);
2278 params.symbol(bybit_symbol.raw_symbol().to_string());
2279
2280 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2281 let body = serde_json::to_vec(¶ms)?;
2282
2283 let _response: crate::common::models::BybitListResponse<serde_json::Value> = self
2284 .inner
2285 .send_request::<_, ()>(Method::POST, "/v5/order/cancel-all", None, Some(body), true)
2286 .await?;
2287
2288 let mut query_params = BybitOrderHistoryParamsBuilder::default();
2290 query_params.category(product_type);
2291 query_params.symbol(bybit_symbol.raw_symbol().to_string());
2292 query_params.limit(50u32);
2293
2294 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
2295 let order_response: BybitOrderHistoryResponse = self
2296 .inner
2297 .send_request(
2298 Method::GET,
2299 "/v5/order/history",
2300 Some(&query_params),
2301 None,
2302 true,
2303 )
2304 .await?;
2305
2306 let ts_init = self.generate_ts_init();
2307
2308 let mut reports = Vec::new();
2309 for order in order_response.result.list {
2310 if let Ok(report) = parse_order_status_report(&order, &instrument, account_id, ts_init)
2311 {
2312 reports.push(report);
2313 }
2314 }
2315
2316 Ok(reports)
2317 }
2318
2319 #[allow(clippy::too_many_arguments)]
2330 pub async fn modify_order(
2331 &self,
2332 account_id: AccountId,
2333 product_type: BybitProductType,
2334 instrument_id: InstrumentId,
2335 client_order_id: Option<ClientOrderId>,
2336 venue_order_id: Option<VenueOrderId>,
2337 quantity: Option<Quantity>,
2338 price: Option<Price>,
2339 ) -> anyhow::Result<OrderStatusReport> {
2340 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2341 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2342
2343 let mut amend_entry = BybitBatchAmendOrderEntryBuilder::default();
2344 amend_entry.symbol(bybit_symbol.raw_symbol().to_string());
2345
2346 if let Some(venue_order_id) = venue_order_id {
2347 amend_entry.order_id(venue_order_id.to_string());
2348 } else if let Some(client_order_id) = client_order_id {
2349 amend_entry.order_link_id(client_order_id.to_string());
2350 } else {
2351 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2352 }
2353
2354 if let Some(quantity) = quantity {
2355 amend_entry.qty(Some(quantity.to_string()));
2356 }
2357
2358 if let Some(price) = price {
2359 amend_entry.price(Some(price.to_string()));
2360 }
2361
2362 let amend_entry = amend_entry.build().map_err(|e| anyhow::anyhow!(e))?;
2363
2364 let mut params = BybitAmendOrderParamsBuilder::default();
2365 params.category(product_type);
2366 params.order(amend_entry);
2367
2368 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2369 let body = serde_json::to_vec(¶ms)?;
2370
2371 let response: BybitPlaceOrderResponse = self
2372 .inner
2373 .send_request::<_, ()>(Method::POST, "/v5/order/amend", None, Some(body), true)
2374 .await?;
2375
2376 let order_id = response
2377 .result
2378 .order_id
2379 .ok_or_else(|| anyhow::anyhow!("No order_id in amend response"))?;
2380
2381 let mut query_params = BybitOpenOrdersParamsBuilder::default();
2383 query_params.category(product_type);
2384 query_params.order_id(order_id.as_str().to_string());
2385
2386 let query_params = query_params.build().map_err(|e| anyhow::anyhow!(e))?;
2387 let order_response: BybitOpenOrdersResponse = self
2388 .inner
2389 .send_request(
2390 Method::GET,
2391 "/v5/order/realtime",
2392 Some(&query_params),
2393 None,
2394 true,
2395 )
2396 .await?;
2397
2398 let order = order_response
2399 .result
2400 .list
2401 .into_iter()
2402 .next()
2403 .ok_or_else(|| anyhow::anyhow!("No order returned after modification"))?;
2404
2405 let ts_init = self.generate_ts_init();
2406
2407 parse_order_status_report(&order, &instrument, account_id, ts_init)
2408 }
2409
2410 pub async fn query_order(
2419 &self,
2420 account_id: AccountId,
2421 product_type: BybitProductType,
2422 instrument_id: InstrumentId,
2423 client_order_id: Option<ClientOrderId>,
2424 venue_order_id: Option<VenueOrderId>,
2425 ) -> anyhow::Result<Option<OrderStatusReport>> {
2426 log::debug!(
2427 "query_order: instrument_id={instrument_id}, client_order_id={client_order_id:?}, venue_order_id={venue_order_id:?}"
2428 );
2429
2430 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2431
2432 let mut params = BybitOpenOrdersParamsBuilder::default();
2433 params.category(product_type);
2434 params.symbol(bybit_symbol.raw_symbol().to_string());
2436
2437 if let Some(venue_order_id) = venue_order_id {
2438 params.order_id(venue_order_id.to_string());
2439 } else if let Some(client_order_id) = client_order_id {
2440 params.order_link_id(client_order_id.to_string());
2441 } else {
2442 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2443 }
2444
2445 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2446 let mut response: BybitOpenOrdersResponse = self
2447 .inner
2448 .send_request(Method::GET, "/v5/order/realtime", Some(¶ms), None, true)
2449 .await?;
2450
2451 if response.result.list.is_empty() {
2452 log::debug!("Order not found in open orders, trying with StopOrder filter");
2453
2454 let mut stop_params = BybitOpenOrdersParamsBuilder::default();
2455 stop_params.category(product_type);
2456 stop_params.symbol(bybit_symbol.raw_symbol().to_string());
2457 stop_params.order_filter(BybitOrderFilter::StopOrder);
2458
2459 if let Some(venue_order_id) = venue_order_id {
2460 stop_params.order_id(venue_order_id.to_string());
2461 } else if let Some(client_order_id) = client_order_id {
2462 stop_params.order_link_id(client_order_id.to_string());
2463 }
2464
2465 let stop_params = stop_params.build().map_err(|e| anyhow::anyhow!(e))?;
2466 response = self
2467 .inner
2468 .send_request(
2469 Method::GET,
2470 "/v5/order/realtime",
2471 Some(&stop_params),
2472 None,
2473 true,
2474 )
2475 .await?;
2476 }
2477
2478 if response.result.list.is_empty() {
2480 log::debug!("Order not found in open orders, checking order history");
2481
2482 let mut history_params = BybitOrderHistoryParamsBuilder::default();
2483 history_params.category(product_type);
2484 history_params.symbol(bybit_symbol.raw_symbol().to_string());
2485
2486 if let Some(venue_order_id) = venue_order_id {
2487 history_params.order_id(venue_order_id.to_string());
2488 } else if let Some(client_order_id) = client_order_id {
2489 history_params.order_link_id(client_order_id.to_string());
2490 }
2491
2492 let history_params = history_params.build().map_err(|e| anyhow::anyhow!(e))?;
2493
2494 let mut history_response: BybitOrderHistoryResponse = self
2495 .inner
2496 .send_request(
2497 Method::GET,
2498 "/v5/order/history",
2499 Some(&history_params),
2500 None,
2501 true,
2502 )
2503 .await?;
2504
2505 if history_response.result.list.is_empty() {
2506 log::debug!("Order not found in order history, trying with StopOrder filter");
2507
2508 let mut stop_history_params = BybitOrderHistoryParamsBuilder::default();
2509 stop_history_params.category(product_type);
2510 stop_history_params.symbol(bybit_symbol.raw_symbol().to_string());
2511 stop_history_params.order_filter(BybitOrderFilter::StopOrder);
2512
2513 if let Some(venue_order_id) = venue_order_id {
2514 stop_history_params.order_id(venue_order_id.to_string());
2515 } else if let Some(client_order_id) = client_order_id {
2516 stop_history_params.order_link_id(client_order_id.to_string());
2517 }
2518
2519 let stop_history_params = stop_history_params
2520 .build()
2521 .map_err(|e| anyhow::anyhow!(e))?;
2522
2523 history_response = self
2524 .inner
2525 .send_request(
2526 Method::GET,
2527 "/v5/order/history",
2528 Some(&stop_history_params),
2529 None,
2530 true,
2531 )
2532 .await?;
2533
2534 if history_response.result.list.is_empty() {
2535 log::debug!("Order not found in order history with StopOrder filter either");
2536 return Ok(None);
2537 }
2538 }
2539
2540 response.result.list = history_response.result.list;
2542 }
2543
2544 let order = &response.result.list[0];
2545 let ts_init = self.generate_ts_init();
2546
2547 log::debug!(
2548 "Query order response: symbol={}, order_id={}, order_link_id={}",
2549 order.symbol.as_str(),
2550 order.order_id.as_str(),
2551 order.order_link_id.as_str()
2552 );
2553
2554 let instrument = self
2555 .instrument_from_cache(&instrument_id.symbol)
2556 .map_err(|e| {
2557 log::error!(
2558 "Instrument cache miss for symbol '{}': {}",
2559 instrument_id.symbol.as_str(),
2560 e
2561 );
2562 anyhow::anyhow!(
2563 "Failed to query order {}: {}",
2564 client_order_id
2565 .as_ref()
2566 .map(|id| id.to_string())
2567 .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
2568 .unwrap_or_else(|| "unknown".to_string()),
2569 e
2570 )
2571 })?;
2572
2573 log::debug!("Retrieved instrument from cache: id={}", instrument.id());
2574
2575 let report =
2576 parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
2577 log::error!(
2578 "Failed to parse order status report for {}: {}",
2579 order.order_link_id.as_str(),
2580 e
2581 );
2582 e
2583 })?;
2584
2585 log::debug!(
2586 "Successfully created OrderStatusReport for {}",
2587 order.order_link_id.as_str()
2588 );
2589
2590 Ok(Some(report))
2591 }
2592
2593 pub async fn request_instruments(
2599 &self,
2600 product_type: BybitProductType,
2601 symbol: Option<String>,
2602 ) -> anyhow::Result<Vec<InstrumentAny>> {
2603 let ts_init = self.generate_ts_init();
2604
2605 let mut instruments = Vec::new();
2606
2607 let default_fee_rate = |symbol: ustr::Ustr| BybitFeeRate {
2608 symbol,
2609 taker_fee_rate: "0.001".to_string(),
2610 maker_fee_rate: "0.001".to_string(),
2611 base_coin: None,
2612 };
2613
2614 match product_type {
2615 BybitProductType::Spot => {
2616 let fee_map: AHashMap<_, _> = {
2618 let mut fee_params = BybitFeeRateParamsBuilder::default();
2619 fee_params.category(product_type);
2620 if let Ok(params) = fee_params.build() {
2621 match self.inner.get_fee_rate(¶ms).await {
2622 Ok(fee_response) => fee_response
2623 .result
2624 .list
2625 .into_iter()
2626 .map(|f| (f.symbol, f))
2627 .collect(),
2628 Err(BybitHttpError::MissingCredentials) => {
2629 log::warn!("Missing credentials for fee rates, using defaults");
2630 AHashMap::new()
2631 }
2632 Err(e) => return Err(e.into()),
2633 }
2634 } else {
2635 AHashMap::new()
2636 }
2637 };
2638
2639 let mut cursor: Option<String> = None;
2640
2641 loop {
2642 let params = BybitInstrumentsInfoParams {
2643 category: product_type,
2644 symbol: symbol.clone(),
2645 status: None,
2646 base_coin: None,
2647 limit: Some(1000),
2648 cursor: cursor.clone(),
2649 };
2650
2651 let response: BybitInstrumentSpotResponse =
2652 self.inner.get_instruments(¶ms).await?;
2653
2654 for definition in response.result.list {
2655 let fee_rate = fee_map
2656 .get(&definition.symbol)
2657 .cloned()
2658 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2659 if let Ok(instrument) =
2660 parse_spot_instrument(&definition, &fee_rate, ts_init, ts_init)
2661 {
2662 instruments.push(instrument);
2663 }
2664 }
2665
2666 cursor = response.result.next_page_cursor;
2667 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2668 break;
2669 }
2670 }
2671 }
2672 BybitProductType::Linear => {
2673 let fee_map: AHashMap<_, _> = {
2675 let mut fee_params = BybitFeeRateParamsBuilder::default();
2676 fee_params.category(product_type);
2677 if let Ok(params) = fee_params.build() {
2678 match self.inner.get_fee_rate(¶ms).await {
2679 Ok(fee_response) => fee_response
2680 .result
2681 .list
2682 .into_iter()
2683 .map(|f| (f.symbol, f))
2684 .collect(),
2685 Err(BybitHttpError::MissingCredentials) => {
2686 log::warn!("Missing credentials for fee rates, using defaults");
2687 AHashMap::new()
2688 }
2689 Err(e) => return Err(e.into()),
2690 }
2691 } else {
2692 AHashMap::new()
2693 }
2694 };
2695
2696 let mut cursor: Option<String> = None;
2697
2698 loop {
2699 let params = BybitInstrumentsInfoParams {
2700 category: product_type,
2701 symbol: symbol.clone(),
2702 status: None,
2703 base_coin: None,
2704 limit: Some(1000),
2705 cursor: cursor.clone(),
2706 };
2707
2708 let response: BybitInstrumentLinearResponse =
2709 self.inner.get_instruments(¶ms).await?;
2710
2711 for definition in response.result.list {
2712 let fee_rate = fee_map
2713 .get(&definition.symbol)
2714 .cloned()
2715 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2716 if let Ok(instrument) =
2717 parse_linear_instrument(&definition, &fee_rate, ts_init, ts_init)
2718 {
2719 instruments.push(instrument);
2720 }
2721 }
2722
2723 cursor = response.result.next_page_cursor;
2724 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2725 break;
2726 }
2727 }
2728 }
2729 BybitProductType::Inverse => {
2730 let fee_map: AHashMap<_, _> = {
2732 let mut fee_params = BybitFeeRateParamsBuilder::default();
2733 fee_params.category(product_type);
2734 if let Ok(params) = fee_params.build() {
2735 match self.inner.get_fee_rate(¶ms).await {
2736 Ok(fee_response) => fee_response
2737 .result
2738 .list
2739 .into_iter()
2740 .map(|f| (f.symbol, f))
2741 .collect(),
2742 Err(BybitHttpError::MissingCredentials) => {
2743 log::warn!("Missing credentials for fee rates, using defaults");
2744 AHashMap::new()
2745 }
2746 Err(e) => return Err(e.into()),
2747 }
2748 } else {
2749 AHashMap::new()
2750 }
2751 };
2752
2753 let mut cursor: Option<String> = None;
2754
2755 loop {
2756 let params = BybitInstrumentsInfoParams {
2757 category: product_type,
2758 symbol: symbol.clone(),
2759 status: None,
2760 base_coin: None,
2761 limit: Some(1000),
2762 cursor: cursor.clone(),
2763 };
2764
2765 let response: BybitInstrumentInverseResponse =
2766 self.inner.get_instruments(¶ms).await?;
2767
2768 for definition in response.result.list {
2769 let fee_rate = fee_map
2770 .get(&definition.symbol)
2771 .cloned()
2772 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2773 if let Ok(instrument) =
2774 parse_inverse_instrument(&definition, &fee_rate, ts_init, ts_init)
2775 {
2776 instruments.push(instrument);
2777 }
2778 }
2779
2780 cursor = response.result.next_page_cursor;
2781 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2782 break;
2783 }
2784 }
2785 }
2786 BybitProductType::Option => {
2787 let mut cursor: Option<String> = None;
2788
2789 loop {
2790 let params = BybitInstrumentsInfoParams {
2791 category: product_type,
2792 symbol: symbol.clone(),
2793 status: None,
2794 base_coin: None,
2795 limit: Some(1000),
2796 cursor: cursor.clone(),
2797 };
2798
2799 let response: BybitInstrumentOptionResponse =
2800 self.inner.get_instruments(¶ms).await?;
2801
2802 for definition in response.result.list {
2803 if let Ok(instrument) =
2804 parse_option_instrument(&definition, ts_init, ts_init)
2805 {
2806 instruments.push(instrument);
2807 }
2808 }
2809
2810 cursor = response.result.next_page_cursor;
2811 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2812 break;
2813 }
2814 }
2815 }
2816 }
2817
2818 for instrument in &instruments {
2819 self.cache_instrument(instrument.clone());
2820 }
2821
2822 Ok(instruments)
2823 }
2824
2825 pub async fn request_tickers(
2838 &self,
2839 params: &BybitTickersParams,
2840 ) -> anyhow::Result<Vec<BybitTickerData>> {
2841 use super::models::{
2842 BybitTickersLinearResponse, BybitTickersOptionResponse, BybitTickersSpotResponse,
2843 };
2844
2845 match params.category {
2846 BybitProductType::Spot => {
2847 let response: BybitTickersSpotResponse = self.inner.get_tickers(params).await?;
2848 Ok(response.result.list.into_iter().map(Into::into).collect())
2849 }
2850 BybitProductType::Linear | BybitProductType::Inverse => {
2851 let response: BybitTickersLinearResponse = self.inner.get_tickers(params).await?;
2852 Ok(response.result.list.into_iter().map(Into::into).collect())
2853 }
2854 BybitProductType::Option => {
2855 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
2856 Ok(response.result.list.into_iter().map(Into::into).collect())
2857 }
2858 }
2859 }
2860
2861 pub async fn request_trades(
2881 &self,
2882 product_type: BybitProductType,
2883 instrument_id: InstrumentId,
2884 limit: Option<u32>,
2885 ) -> anyhow::Result<Vec<TradeTick>> {
2886 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2887 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2888
2889 let mut params_builder = BybitTradesParamsBuilder::default();
2890 params_builder.category(product_type);
2891 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
2892 if let Some(limit_val) = limit {
2893 params_builder.limit(limit_val);
2894 }
2895
2896 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
2897 let response = self.inner.get_recent_trades(¶ms).await?;
2898
2899 let ts_init = self.generate_ts_init();
2900 let mut trades = Vec::new();
2901
2902 for trade in response.result.list {
2903 if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, ts_init) {
2904 trades.push(trade_tick);
2905 }
2906 }
2907
2908 Ok(trades)
2909 }
2910
2911 pub async fn request_bars(
2924 &self,
2925 product_type: BybitProductType,
2926 bar_type: BarType,
2927 start: Option<DateTime<Utc>>,
2928 end: Option<DateTime<Utc>>,
2929 limit: Option<u32>,
2930 timestamp_on_close: bool,
2931 ) -> anyhow::Result<Vec<Bar>> {
2932 let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
2933 let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
2934
2935 let interval = bar_spec_to_bybit_interval(
2937 bar_type.spec().aggregation,
2938 bar_type.spec().step.get() as u64,
2939 )?;
2940
2941 let start_ms = start.map(|dt| dt.timestamp_millis());
2942 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
2943 let current_time_ms = get_atomic_clock_realtime().get_time_ms() as i64;
2944
2945 let mut pages: Vec<Vec<Bar>> = Vec::new();
2955 let mut total_bars = 0usize;
2956 let mut current_end = end.map(|dt| dt.timestamp_millis());
2957 let mut page_count = 0;
2958
2959 loop {
2960 page_count += 1;
2961
2962 let mut params_builder = BybitKlinesParamsBuilder::default();
2963 params_builder.category(product_type);
2964 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
2965 params_builder.interval(interval);
2966 params_builder.limit(1000u32); if let Some(start_val) = start_ms {
2969 params_builder.start(start_val);
2970 }
2971 if let Some(end_val) = current_end {
2972 params_builder.end(end_val);
2973 }
2974
2975 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
2976 let response = self.inner.get_klines(¶ms).await?;
2977
2978 let klines = response.result.list;
2979 if klines.is_empty() {
2980 break;
2981 }
2982
2983 let mut klines_with_ts: Vec<(i64, _)> = klines
2985 .into_iter()
2986 .filter_map(|k| k.start.parse::<i64>().ok().map(|ts| (ts, k)))
2987 .collect();
2988
2989 klines_with_ts.sort_by_key(|(ts, _)| *ts);
2990
2991 let has_new = klines_with_ts
2993 .iter()
2994 .any(|(ts, _)| !seen_timestamps.contains(ts));
2995 if !has_new {
2996 break;
2997 }
2998
2999 let ts_init = self.generate_ts_init();
3000 let mut page_bars = Vec::with_capacity(klines_with_ts.len());
3001
3002 let mut earliest_ts: Option<i64> = None;
3003
3004 for (start_time, kline) in &klines_with_ts {
3005 if earliest_ts.is_none_or(|ts| *start_time < ts) {
3007 earliest_ts = Some(*start_time);
3008 }
3009
3010 let bar_end_time = interval.bar_end_time_ms(*start_time);
3011 if bar_end_time > current_time_ms {
3012 continue;
3013 }
3014
3015 if !seen_timestamps.contains(start_time)
3016 && let Ok(bar) =
3017 parse_kline_bar(kline, &instrument, bar_type, timestamp_on_close, ts_init)
3018 {
3019 page_bars.push(bar);
3020 seen_timestamps.insert(*start_time);
3021 }
3022 }
3023
3024 total_bars += page_bars.len();
3027 pages.push(page_bars);
3028
3029 if let Some(limit_val) = limit
3031 && total_bars >= limit_val as usize
3032 {
3033 break;
3034 }
3035
3036 let Some(earliest_bar_time) = earliest_ts else {
3039 break;
3040 };
3041 if let Some(start_val) = start_ms
3042 && earliest_bar_time <= start_val
3043 {
3044 break;
3045 }
3046
3047 current_end = Some(earliest_bar_time - 1);
3048
3049 if page_count > 100 {
3051 break;
3052 }
3053 }
3054
3055 let mut all_bars: Vec<Bar> = Vec::with_capacity(total_bars);
3057 for page in pages.into_iter().rev() {
3058 all_bars.extend(page);
3059 }
3060
3061 if let Some(limit_val) = limit {
3063 let limit_usize = limit_val as usize;
3064 if all_bars.len() > limit_usize {
3065 let start_idx = all_bars.len() - limit_usize;
3066 return Ok(all_bars[start_idx..].to_vec());
3067 }
3068 }
3069
3070 Ok(all_bars)
3071 }
3072
3073 pub async fn request_fee_rates(
3085 &self,
3086 product_type: BybitProductType,
3087 symbol: Option<String>,
3088 base_coin: Option<String>,
3089 ) -> anyhow::Result<Vec<BybitFeeRate>> {
3090 let params = BybitFeeRateParams {
3091 category: product_type,
3092 symbol,
3093 base_coin,
3094 };
3095
3096 let response = self.inner.get_fee_rate(¶ms).await?;
3097 Ok(response.result.list)
3098 }
3099
3100 pub async fn request_account_state(
3112 &self,
3113 account_type: BybitAccountType,
3114 account_id: AccountId,
3115 ) -> anyhow::Result<AccountState> {
3116 let params = BybitWalletBalanceParams {
3117 account_type,
3118 coin: None,
3119 };
3120
3121 let response = self.inner.get_wallet_balance(¶ms).await?;
3122 let ts_init = self.generate_ts_init();
3123
3124 let wallet_balance = response
3126 .result
3127 .list
3128 .first()
3129 .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
3130
3131 parse_account_state(wallet_balance, account_id, ts_init)
3132 }
3133
3134 #[allow(clippy::too_many_arguments)]
3145 pub async fn request_order_status_reports(
3146 &self,
3147 account_id: AccountId,
3148 product_type: BybitProductType,
3149 instrument_id: Option<InstrumentId>,
3150 open_only: bool,
3151 start: Option<DateTime<Utc>>,
3152 end: Option<DateTime<Utc>>,
3153 limit: Option<u32>,
3154 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3155 let symbol_param = if let Some(id) = instrument_id.as_ref() {
3157 let symbol_str = id.symbol.as_str();
3158 if symbol_str.is_empty() {
3159 None
3160 } else {
3161 Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
3162 }
3163 } else {
3164 None
3165 };
3166
3167 let settle_coins_to_query: Vec<Option<String>> =
3170 if product_type == BybitProductType::Linear && symbol_param.is_none() {
3171 vec![Some("USDT".to_string()), Some("USDC".to_string())]
3172 } else {
3173 match product_type {
3174 BybitProductType::Inverse => vec![None],
3175 _ => vec![None],
3176 }
3177 };
3178
3179 let mut all_collected_orders = Vec::new();
3180 let mut total_collected_across_coins = 0;
3181
3182 for settle_coin in settle_coins_to_query {
3183 let remaining_limit = if let Some(limit) = limit {
3184 let remaining = (limit as usize).saturating_sub(total_collected_across_coins);
3185 if remaining == 0 {
3186 break;
3187 }
3188 Some(remaining as u32)
3189 } else {
3190 None
3191 };
3192
3193 let orders_for_coin = if open_only {
3194 let mut all_orders = Vec::new();
3195 let mut cursor: Option<String> = None;
3196 let mut total_orders = 0;
3197
3198 loop {
3199 let remaining = if let Some(limit) = remaining_limit {
3200 (limit as usize).saturating_sub(total_orders)
3201 } else {
3202 usize::MAX
3203 };
3204
3205 if remaining == 0 {
3206 break;
3207 }
3208
3209 let page_limit = std::cmp::min(remaining, 50);
3211
3212 let mut p = BybitOpenOrdersParamsBuilder::default();
3213 p.category(product_type);
3214 if let Some(symbol) = symbol_param.clone() {
3215 p.symbol(symbol);
3216 }
3217 if let Some(coin) = settle_coin.clone() {
3218 p.settle_coin(coin);
3219 }
3220 p.limit(page_limit as u32);
3221 if let Some(c) = cursor {
3222 p.cursor(c);
3223 }
3224 let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
3225 let response: BybitOpenOrdersResponse = self
3226 .inner
3227 .send_request(Method::GET, "/v5/order/realtime", Some(¶ms), None, true)
3228 .await?;
3229
3230 total_orders += response.result.list.len();
3231 all_orders.extend(response.result.list);
3232
3233 cursor = response.result.next_page_cursor;
3234 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3235 break;
3236 }
3237 }
3238
3239 all_orders
3240 } else {
3241 let mut all_orders = Vec::new();
3244 let mut open_orders = Vec::new();
3245 let mut cursor: Option<String> = None;
3246 let mut total_open_orders = 0;
3247
3248 loop {
3249 let remaining = if let Some(limit) = remaining_limit {
3250 (limit as usize).saturating_sub(total_open_orders)
3251 } else {
3252 usize::MAX
3253 };
3254
3255 if remaining == 0 {
3256 break;
3257 }
3258
3259 let page_limit = std::cmp::min(remaining, 50);
3261
3262 let mut open_params = BybitOpenOrdersParamsBuilder::default();
3263 open_params.category(product_type);
3264 if let Some(symbol) = symbol_param.clone() {
3265 open_params.symbol(symbol);
3266 }
3267 if let Some(coin) = settle_coin.clone() {
3268 open_params.settle_coin(coin);
3269 }
3270 open_params.limit(page_limit as u32);
3271 if let Some(c) = cursor {
3272 open_params.cursor(c);
3273 }
3274 let open_params = open_params.build().map_err(|e| anyhow::anyhow!(e))?;
3275 let open_response: BybitOpenOrdersResponse = self
3276 .inner
3277 .send_request(
3278 Method::GET,
3279 "/v5/order/realtime",
3280 Some(&open_params),
3281 None,
3282 true,
3283 )
3284 .await?;
3285
3286 total_open_orders += open_response.result.list.len();
3287 open_orders.extend(open_response.result.list);
3288
3289 cursor = open_response.result.next_page_cursor;
3290 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3291 break;
3292 }
3293 }
3294
3295 let seen_order_ids: AHashSet<Ustr> =
3296 open_orders.iter().map(|o| o.order_id).collect();
3297
3298 all_orders.extend(open_orders);
3299
3300 let mut cursor: Option<String> = None;
3301 let mut total_history_orders = 0;
3302
3303 loop {
3304 let total_orders = total_open_orders + total_history_orders;
3305 let remaining = if let Some(limit) = remaining_limit {
3306 (limit as usize).saturating_sub(total_orders)
3307 } else {
3308 usize::MAX
3309 };
3310
3311 if remaining == 0 {
3312 break;
3313 }
3314
3315 let page_limit = std::cmp::min(remaining, 50);
3317
3318 let mut history_params = BybitOrderHistoryParamsBuilder::default();
3319 history_params.category(product_type);
3320 if let Some(symbol) = symbol_param.clone() {
3321 history_params.symbol(symbol);
3322 }
3323 if let Some(coin) = settle_coin.clone() {
3324 history_params.settle_coin(coin);
3325 }
3326 if let Some(start) = start {
3327 history_params.start_time(start.timestamp_millis());
3328 }
3329 if let Some(end) = end {
3330 history_params.end_time(end.timestamp_millis());
3331 }
3332 history_params.limit(page_limit as u32);
3333 if let Some(c) = cursor {
3334 history_params.cursor(c);
3335 }
3336 let history_params = history_params.build().map_err(|e| anyhow::anyhow!(e))?;
3337 let history_response: BybitOrderHistoryResponse = self
3338 .inner
3339 .send_request(
3340 Method::GET,
3341 "/v5/order/history",
3342 Some(&history_params),
3343 None,
3344 true,
3345 )
3346 .await?;
3347
3348 for order in history_response.result.list {
3350 if !seen_order_ids.contains(&order.order_id) {
3351 all_orders.push(order);
3352 total_history_orders += 1;
3353 }
3354 }
3355
3356 cursor = history_response.result.next_page_cursor;
3357 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3358 break;
3359 }
3360 }
3361
3362 all_orders
3363 };
3364
3365 total_collected_across_coins += orders_for_coin.len();
3366 all_collected_orders.extend(orders_for_coin);
3367 }
3368
3369 let ts_init = self.generate_ts_init();
3370
3371 let mut reports = Vec::new();
3372 for order in all_collected_orders {
3373 if let Some(ref instrument_id) = instrument_id {
3374 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3375 if let Ok(report) =
3376 parse_order_status_report(&order, &instrument, account_id, ts_init)
3377 {
3378 reports.push(report);
3379 }
3380 } else {
3381 if !order.symbol.is_empty() {
3384 let symbol_with_product =
3385 Symbol::from_ustr_unchecked(make_bybit_symbol(order.symbol, product_type));
3386
3387 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3388 log::debug!(
3389 "Skipping order report for instrument not in cache: symbol={}, full_symbol={}",
3390 order.symbol,
3391 symbol_with_product
3392 );
3393 continue;
3394 };
3395
3396 match parse_order_status_report(&order, &instrument, account_id, ts_init) {
3397 Ok(report) => reports.push(report),
3398 Err(e) => {
3399 log::error!("Failed to parse order status report: {e}");
3400 }
3401 }
3402 }
3403 }
3404 }
3405
3406 Ok(reports)
3407 }
3408
3409 pub async fn request_fill_reports(
3421 &self,
3422 account_id: AccountId,
3423 product_type: BybitProductType,
3424 instrument_id: Option<InstrumentId>,
3425 start: Option<i64>,
3426 end: Option<i64>,
3427 limit: Option<u32>,
3428 ) -> anyhow::Result<Vec<FillReport>> {
3429 let symbol = if let Some(id) = instrument_id {
3431 let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
3432 Some(bybit_symbol.raw_symbol().to_string())
3433 } else {
3434 None
3435 };
3436
3437 let mut all_executions = Vec::new();
3439 let mut cursor: Option<String> = None;
3440 let mut total_executions = 0;
3441
3442 loop {
3443 let remaining = if let Some(limit) = limit {
3445 (limit as usize).saturating_sub(total_executions)
3446 } else {
3447 usize::MAX
3448 };
3449
3450 if remaining == 0 {
3452 break;
3453 }
3454
3455 let page_limit = std::cmp::min(remaining, 100);
3457
3458 let params = BybitTradeHistoryParams {
3459 category: product_type,
3460 symbol: symbol.clone(),
3461 base_coin: None,
3462 order_id: None,
3463 order_link_id: None,
3464 start_time: start,
3465 end_time: end,
3466 exec_type: None,
3467 limit: Some(page_limit as u32),
3468 cursor: cursor.clone(),
3469 };
3470
3471 let response = self.inner.get_trade_history(¶ms).await?;
3472 let list_len = response.result.list.len();
3473 all_executions.extend(response.result.list);
3474 total_executions += list_len;
3475
3476 cursor = response.result.next_page_cursor;
3477 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3478 break;
3479 }
3480 }
3481
3482 let ts_init = self.generate_ts_init();
3483 let mut reports = Vec::new();
3484
3485 for execution in all_executions {
3486 let symbol_with_product =
3489 Symbol::from_ustr_unchecked(make_bybit_symbol(execution.symbol, product_type));
3490
3491 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3492 log::debug!(
3493 "Skipping fill report for instrument not in cache: symbol={}, full_symbol={}",
3494 execution.symbol,
3495 symbol_with_product
3496 );
3497 continue;
3498 };
3499
3500 match parse_fill_report(&execution, account_id, &instrument, ts_init) {
3501 Ok(report) => reports.push(report),
3502 Err(e) => {
3503 log::error!("Failed to parse fill report: {e}");
3504 }
3505 }
3506 }
3507
3508 Ok(reports)
3509 }
3510
3511 pub async fn request_position_status_reports(
3523 &self,
3524 account_id: AccountId,
3525 product_type: BybitProductType,
3526 instrument_id: Option<InstrumentId>,
3527 ) -> anyhow::Result<Vec<PositionStatusReport>> {
3528 if product_type == BybitProductType::Spot {
3530 if self.use_spot_position_reports.load(Ordering::Relaxed) {
3531 return self
3532 .generate_spot_position_reports_from_wallet(account_id, instrument_id)
3533 .await;
3534 } else {
3535 return Ok(Vec::new());
3537 }
3538 }
3539
3540 let ts_init = self.generate_ts_init();
3541 let mut reports = Vec::new();
3542
3543 let symbol = if let Some(id) = instrument_id {
3545 let symbol_str = id.symbol.as_str();
3546 if symbol_str.is_empty() {
3547 anyhow::bail!("InstrumentId symbol is empty");
3548 }
3549 let bybit_symbol = BybitSymbol::new(symbol_str)?;
3550 Some(bybit_symbol.raw_symbol().to_string())
3551 } else {
3552 None
3553 };
3554
3555 if product_type == BybitProductType::Linear && symbol.is_none() {
3558 for settle_coin in ["USDT", "USDC"] {
3560 let mut cursor: Option<String> = None;
3561
3562 loop {
3563 let params = BybitPositionListParams {
3564 category: product_type,
3565 symbol: None,
3566 base_coin: None,
3567 settle_coin: Some(settle_coin.to_string()),
3568 limit: Some(200), cursor: cursor.clone(),
3570 };
3571
3572 let response = self.inner.get_positions(¶ms).await?;
3573
3574 for position in response.result.list {
3575 if position.symbol.is_empty() {
3576 continue;
3577 }
3578
3579 let symbol_with_product = Symbol::new(format!(
3580 "{}{}",
3581 position.symbol.as_str(),
3582 product_type.suffix()
3583 ));
3584
3585 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
3586 else {
3587 log::debug!(
3588 "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
3589 position.symbol,
3590 symbol_with_product
3591 );
3592 continue;
3593 };
3594
3595 match parse_position_status_report(
3596 &position,
3597 account_id,
3598 &instrument,
3599 ts_init,
3600 ) {
3601 Ok(report) => reports.push(report),
3602 Err(e) => {
3603 log::error!("Failed to parse position status report: {e}");
3604 }
3605 }
3606 }
3607
3608 cursor = response.result.next_page_cursor;
3609 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3610 break;
3611 }
3612 }
3613 }
3614 } else {
3615 let mut cursor: Option<String> = None;
3617
3618 loop {
3619 let params = BybitPositionListParams {
3620 category: product_type,
3621 symbol: symbol.clone(),
3622 base_coin: None,
3623 settle_coin: None,
3624 limit: Some(200), cursor: cursor.clone(),
3626 };
3627
3628 let response = self.inner.get_positions(¶ms).await?;
3629
3630 for position in response.result.list {
3631 if position.symbol.is_empty() {
3632 continue;
3633 }
3634
3635 let symbol_with_product = Symbol::new(format!(
3636 "{}{}",
3637 position.symbol.as_str(),
3638 product_type.suffix()
3639 ));
3640
3641 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3642 log::debug!(
3643 "Skipping position report for instrument not in cache: symbol={}, full_symbol={}",
3644 position.symbol,
3645 symbol_with_product
3646 );
3647 continue;
3648 };
3649
3650 match parse_position_status_report(&position, account_id, &instrument, ts_init)
3651 {
3652 Ok(report) => reports.push(report),
3653 Err(e) => {
3654 log::error!("Failed to parse position status report: {e}");
3655 }
3656 }
3657 }
3658
3659 cursor = response.result.next_page_cursor;
3660 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3661 break;
3662 }
3663 }
3664 }
3665
3666 Ok(reports)
3667 }
3668}
3669
3670#[cfg(test)]
3671mod tests {
3672 use rstest::rstest;
3673
3674 use super::*;
3675
3676 #[rstest]
3677 fn test_client_creation() {
3678 let client = BybitHttpClient::new(None, Some(60), None, None, None, None, None);
3679 assert!(client.is_ok());
3680
3681 let client = client.unwrap();
3682 assert!(client.base_url().contains("bybit.com"));
3683 assert!(client.credential().is_none());
3684 }
3685
3686 #[rstest]
3687 fn test_client_with_credentials() {
3688 let client = BybitHttpClient::with_credentials(
3689 "test_key".to_string(),
3690 "test_secret".to_string(),
3691 Some("https://api-testnet.bybit.com".to_string()),
3692 Some(60),
3693 None,
3694 None,
3695 None,
3696 None,
3697 None,
3698 );
3699 assert!(client.is_ok());
3700
3701 let client = client.unwrap();
3702 assert!(client.credential().is_some());
3703 }
3704
3705 #[rstest]
3706 fn test_build_path_with_params() {
3707 #[derive(Serialize)]
3708 struct TestParams {
3709 category: String,
3710 symbol: String,
3711 }
3712
3713 let params = TestParams {
3714 category: "linear".to_string(),
3715 symbol: "BTCUSDT".to_string(),
3716 };
3717
3718 let path = BybitRawHttpClient::build_path("/v5/market/test", ¶ms);
3719 assert!(path.is_ok());
3720 assert!(path.unwrap().contains("category=linear"));
3721 }
3722
3723 #[rstest]
3724 fn test_build_path_without_params() {
3725 let params = ();
3726 let path = BybitRawHttpClient::build_path("/v5/market/time", ¶ms);
3727 assert!(path.is_ok());
3728 assert_eq!(path.unwrap(), "/v5/market/time");
3729 }
3730
3731 #[rstest]
3732 fn test_params_serialization_matches_build_path() {
3733 #[derive(Serialize)]
3735 struct TestParams {
3736 category: String,
3737 limit: u32,
3738 }
3739
3740 let params = TestParams {
3741 category: "spot".to_string(),
3742 limit: 50,
3743 };
3744
3745 let old_path = BybitRawHttpClient::build_path("/v5/order/realtime", ¶ms).unwrap();
3747 let old_query = old_path.split('?').nth(1).unwrap_or("");
3748
3749 let new_query = serde_urlencoded::to_string(¶ms).unwrap();
3751
3752 assert_eq!(old_query, new_query);
3754 }
3755
3756 #[rstest]
3757 fn test_params_serialization_order() {
3758 #[derive(Serialize)]
3760 struct OrderParams {
3761 category: String,
3762 symbol: String,
3763 limit: u32,
3764 }
3765
3766 let params = OrderParams {
3767 category: "spot".to_string(),
3768 symbol: "BTCUSDT".to_string(),
3769 limit: 50,
3770 };
3771
3772 let query1 = serde_urlencoded::to_string(¶ms).unwrap();
3774 let query2 = serde_urlencoded::to_string(¶ms).unwrap();
3775 let query3 = serde_urlencoded::to_string(¶ms).unwrap();
3776
3777 assert_eq!(query1, query2);
3778 assert_eq!(query2, query3);
3779
3780 assert!(query1.contains("category=spot"));
3782 assert!(query1.contains("symbol=BTCUSDT"));
3783 assert!(query1.contains("limit=50"));
3784 }
3785}