1use std::{
21 collections::HashMap,
22 fmt::{Debug, Formatter},
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.adapters")
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 Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("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 tracing::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 tracing::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.adapters")
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 Formatter<'_>) -> std::fmt::Result {
1170 f.debug_struct("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 tracing::debug!(
2219 symbol = %instrument_id.symbol,
2220 "Skipping cancelled order report for instrument not in cache"
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 tracing::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 tracing::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 tracing::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 tracing::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 tracing::debug!(
2536 "Order not found in order history with StopOrder filter either"
2537 );
2538 return Ok(None);
2539 }
2540 }
2541
2542 response.result.list = history_response.result.list;
2544 }
2545
2546 let order = &response.result.list[0];
2547 let ts_init = self.generate_ts_init();
2548
2549 tracing::debug!(
2550 "Query order response: symbol={}, order_id={}, order_link_id={}",
2551 order.symbol.as_str(),
2552 order.order_id.as_str(),
2553 order.order_link_id.as_str()
2554 );
2555
2556 let instrument = self
2557 .instrument_from_cache(&instrument_id.symbol)
2558 .map_err(|e| {
2559 tracing::error!(
2560 "Instrument cache miss for symbol '{}': {}",
2561 instrument_id.symbol.as_str(),
2562 e
2563 );
2564 anyhow::anyhow!(
2565 "Failed to query order {}: {}",
2566 client_order_id
2567 .as_ref()
2568 .map(|id| id.to_string())
2569 .or_else(|| venue_order_id.as_ref().map(|id| id.to_string()))
2570 .unwrap_or_else(|| "unknown".to_string()),
2571 e
2572 )
2573 })?;
2574
2575 tracing::debug!("Retrieved instrument from cache: id={}", instrument.id());
2576
2577 let report =
2578 parse_order_status_report(order, &instrument, account_id, ts_init).map_err(|e| {
2579 tracing::error!(
2580 "Failed to parse order status report for {}: {}",
2581 order.order_link_id.as_str(),
2582 e
2583 );
2584 e
2585 })?;
2586
2587 tracing::debug!(
2588 "Successfully created OrderStatusReport for {}",
2589 order.order_link_id.as_str()
2590 );
2591
2592 Ok(Some(report))
2593 }
2594
2595 pub async fn request_instruments(
2601 &self,
2602 product_type: BybitProductType,
2603 symbol: Option<String>,
2604 ) -> anyhow::Result<Vec<InstrumentAny>> {
2605 let ts_init = self.generate_ts_init();
2606
2607 let mut instruments = Vec::new();
2608
2609 let default_fee_rate = |symbol: ustr::Ustr| BybitFeeRate {
2610 symbol,
2611 taker_fee_rate: "0.001".to_string(),
2612 maker_fee_rate: "0.001".to_string(),
2613 base_coin: None,
2614 };
2615
2616 match product_type {
2617 BybitProductType::Spot => {
2618 let fee_map: AHashMap<_, _> = {
2620 let mut fee_params = BybitFeeRateParamsBuilder::default();
2621 fee_params.category(product_type);
2622 if let Ok(params) = fee_params.build() {
2623 match self.inner.get_fee_rate(¶ms).await {
2624 Ok(fee_response) => fee_response
2625 .result
2626 .list
2627 .into_iter()
2628 .map(|f| (f.symbol, f))
2629 .collect(),
2630 Err(BybitHttpError::MissingCredentials) => {
2631 tracing::warn!("Missing credentials for fee rates, using defaults");
2632 AHashMap::new()
2633 }
2634 Err(e) => return Err(e.into()),
2635 }
2636 } else {
2637 AHashMap::new()
2638 }
2639 };
2640
2641 let mut cursor: Option<String> = None;
2642
2643 loop {
2644 let params = BybitInstrumentsInfoParams {
2645 category: product_type,
2646 symbol: symbol.clone(),
2647 status: None,
2648 base_coin: None,
2649 limit: Some(1000),
2650 cursor: cursor.clone(),
2651 };
2652
2653 let response: BybitInstrumentSpotResponse =
2654 self.inner.get_instruments(¶ms).await?;
2655
2656 for definition in response.result.list {
2657 let fee_rate = fee_map
2658 .get(&definition.symbol)
2659 .cloned()
2660 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2661 if let Ok(instrument) =
2662 parse_spot_instrument(&definition, &fee_rate, ts_init, ts_init)
2663 {
2664 instruments.push(instrument);
2665 }
2666 }
2667
2668 cursor = response.result.next_page_cursor;
2669 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2670 break;
2671 }
2672 }
2673 }
2674 BybitProductType::Linear => {
2675 let fee_map: AHashMap<_, _> = {
2677 let mut fee_params = BybitFeeRateParamsBuilder::default();
2678 fee_params.category(product_type);
2679 if let Ok(params) = fee_params.build() {
2680 match self.inner.get_fee_rate(¶ms).await {
2681 Ok(fee_response) => fee_response
2682 .result
2683 .list
2684 .into_iter()
2685 .map(|f| (f.symbol, f))
2686 .collect(),
2687 Err(BybitHttpError::MissingCredentials) => {
2688 tracing::warn!("Missing credentials for fee rates, using defaults");
2689 AHashMap::new()
2690 }
2691 Err(e) => return Err(e.into()),
2692 }
2693 } else {
2694 AHashMap::new()
2695 }
2696 };
2697
2698 let mut cursor: Option<String> = None;
2699
2700 loop {
2701 let params = BybitInstrumentsInfoParams {
2702 category: product_type,
2703 symbol: symbol.clone(),
2704 status: None,
2705 base_coin: None,
2706 limit: Some(1000),
2707 cursor: cursor.clone(),
2708 };
2709
2710 let response: BybitInstrumentLinearResponse =
2711 self.inner.get_instruments(¶ms).await?;
2712
2713 for definition in response.result.list {
2714 let fee_rate = fee_map
2715 .get(&definition.symbol)
2716 .cloned()
2717 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2718 if let Ok(instrument) =
2719 parse_linear_instrument(&definition, &fee_rate, ts_init, ts_init)
2720 {
2721 instruments.push(instrument);
2722 }
2723 }
2724
2725 cursor = response.result.next_page_cursor;
2726 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2727 break;
2728 }
2729 }
2730 }
2731 BybitProductType::Inverse => {
2732 let fee_map: AHashMap<_, _> = {
2734 let mut fee_params = BybitFeeRateParamsBuilder::default();
2735 fee_params.category(product_type);
2736 if let Ok(params) = fee_params.build() {
2737 match self.inner.get_fee_rate(¶ms).await {
2738 Ok(fee_response) => fee_response
2739 .result
2740 .list
2741 .into_iter()
2742 .map(|f| (f.symbol, f))
2743 .collect(),
2744 Err(BybitHttpError::MissingCredentials) => {
2745 tracing::warn!("Missing credentials for fee rates, using defaults");
2746 AHashMap::new()
2747 }
2748 Err(e) => return Err(e.into()),
2749 }
2750 } else {
2751 AHashMap::new()
2752 }
2753 };
2754
2755 let mut cursor: Option<String> = None;
2756
2757 loop {
2758 let params = BybitInstrumentsInfoParams {
2759 category: product_type,
2760 symbol: symbol.clone(),
2761 status: None,
2762 base_coin: None,
2763 limit: Some(1000),
2764 cursor: cursor.clone(),
2765 };
2766
2767 let response: BybitInstrumentInverseResponse =
2768 self.inner.get_instruments(¶ms).await?;
2769
2770 for definition in response.result.list {
2771 let fee_rate = fee_map
2772 .get(&definition.symbol)
2773 .cloned()
2774 .unwrap_or_else(|| default_fee_rate(definition.symbol));
2775 if let Ok(instrument) =
2776 parse_inverse_instrument(&definition, &fee_rate, ts_init, ts_init)
2777 {
2778 instruments.push(instrument);
2779 }
2780 }
2781
2782 cursor = response.result.next_page_cursor;
2783 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2784 break;
2785 }
2786 }
2787 }
2788 BybitProductType::Option => {
2789 let mut cursor: Option<String> = None;
2790
2791 loop {
2792 let params = BybitInstrumentsInfoParams {
2793 category: product_type,
2794 symbol: symbol.clone(),
2795 status: None,
2796 base_coin: None,
2797 limit: Some(1000),
2798 cursor: cursor.clone(),
2799 };
2800
2801 let response: BybitInstrumentOptionResponse =
2802 self.inner.get_instruments(¶ms).await?;
2803
2804 for definition in response.result.list {
2805 if let Ok(instrument) =
2806 parse_option_instrument(&definition, ts_init, ts_init)
2807 {
2808 instruments.push(instrument);
2809 }
2810 }
2811
2812 cursor = response.result.next_page_cursor;
2813 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
2814 break;
2815 }
2816 }
2817 }
2818 }
2819
2820 for instrument in &instruments {
2821 self.cache_instrument(instrument.clone());
2822 }
2823
2824 Ok(instruments)
2825 }
2826
2827 pub async fn request_tickers(
2840 &self,
2841 params: &BybitTickersParams,
2842 ) -> anyhow::Result<Vec<BybitTickerData>> {
2843 use super::models::{
2844 BybitTickersLinearResponse, BybitTickersOptionResponse, BybitTickersSpotResponse,
2845 };
2846
2847 match params.category {
2848 BybitProductType::Spot => {
2849 let response: BybitTickersSpotResponse = self.inner.get_tickers(params).await?;
2850 Ok(response.result.list.into_iter().map(Into::into).collect())
2851 }
2852 BybitProductType::Linear | BybitProductType::Inverse => {
2853 let response: BybitTickersLinearResponse = self.inner.get_tickers(params).await?;
2854 Ok(response.result.list.into_iter().map(Into::into).collect())
2855 }
2856 BybitProductType::Option => {
2857 let response: BybitTickersOptionResponse = self.inner.get_tickers(params).await?;
2858 Ok(response.result.list.into_iter().map(Into::into).collect())
2859 }
2860 }
2861 }
2862
2863 pub async fn request_trades(
2883 &self,
2884 product_type: BybitProductType,
2885 instrument_id: InstrumentId,
2886 limit: Option<u32>,
2887 ) -> anyhow::Result<Vec<TradeTick>> {
2888 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
2889 let bybit_symbol = BybitSymbol::new(instrument_id.symbol.as_str())?;
2890
2891 let mut params_builder = BybitTradesParamsBuilder::default();
2892 params_builder.category(product_type);
2893 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
2894 if let Some(limit_val) = limit {
2895 params_builder.limit(limit_val);
2896 }
2897
2898 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
2899 let response = self.inner.get_recent_trades(¶ms).await?;
2900
2901 let ts_init = self.generate_ts_init();
2902 let mut trades = Vec::new();
2903
2904 for trade in response.result.list {
2905 if let Ok(trade_tick) = parse_trade_tick(&trade, &instrument, ts_init) {
2906 trades.push(trade_tick);
2907 }
2908 }
2909
2910 Ok(trades)
2911 }
2912
2913 pub async fn request_bars(
2926 &self,
2927 product_type: BybitProductType,
2928 bar_type: BarType,
2929 start: Option<DateTime<Utc>>,
2930 end: Option<DateTime<Utc>>,
2931 limit: Option<u32>,
2932 timestamp_on_close: bool,
2933 ) -> anyhow::Result<Vec<Bar>> {
2934 let instrument = self.instrument_from_cache(&bar_type.instrument_id().symbol)?;
2935 let bybit_symbol = BybitSymbol::new(bar_type.instrument_id().symbol.as_str())?;
2936
2937 let interval = bar_spec_to_bybit_interval(
2939 bar_type.spec().aggregation,
2940 bar_type.spec().step.get() as u64,
2941 )?;
2942
2943 let start_ms = start.map(|dt| dt.timestamp_millis());
2944 let mut seen_timestamps: AHashSet<i64> = AHashSet::new();
2945 let current_time_ms = get_atomic_clock_realtime().get_time_ms() as i64;
2946
2947 let mut pages: Vec<Vec<Bar>> = Vec::new();
2957 let mut total_bars = 0usize;
2958 let mut current_end = end.map(|dt| dt.timestamp_millis());
2959 let mut page_count = 0;
2960
2961 loop {
2962 page_count += 1;
2963
2964 let mut params_builder = BybitKlinesParamsBuilder::default();
2965 params_builder.category(product_type);
2966 params_builder.symbol(bybit_symbol.raw_symbol().to_string());
2967 params_builder.interval(interval);
2968 params_builder.limit(1000u32); if let Some(start_val) = start_ms {
2971 params_builder.start(start_val);
2972 }
2973 if let Some(end_val) = current_end {
2974 params_builder.end(end_val);
2975 }
2976
2977 let params = params_builder.build().map_err(|e| anyhow::anyhow!(e))?;
2978 let response = self.inner.get_klines(¶ms).await?;
2979
2980 let klines = response.result.list;
2981 if klines.is_empty() {
2982 break;
2983 }
2984
2985 let mut klines_with_ts: Vec<(i64, _)> = klines
2987 .into_iter()
2988 .filter_map(|k| k.start.parse::<i64>().ok().map(|ts| (ts, k)))
2989 .collect();
2990
2991 klines_with_ts.sort_by_key(|(ts, _)| *ts);
2992
2993 let has_new = klines_with_ts
2995 .iter()
2996 .any(|(ts, _)| !seen_timestamps.contains(ts));
2997 if !has_new {
2998 break;
2999 }
3000
3001 let ts_init = self.generate_ts_init();
3002 let mut page_bars = Vec::with_capacity(klines_with_ts.len());
3003
3004 let mut earliest_ts: Option<i64> = None;
3005
3006 for (start_time, kline) in &klines_with_ts {
3007 if earliest_ts.is_none_or(|ts| *start_time < ts) {
3009 earliest_ts = Some(*start_time);
3010 }
3011
3012 let bar_end_time = interval.bar_end_time_ms(*start_time);
3013 if bar_end_time > current_time_ms {
3014 continue;
3015 }
3016
3017 if !seen_timestamps.contains(start_time)
3018 && let Ok(bar) =
3019 parse_kline_bar(kline, &instrument, bar_type, timestamp_on_close, ts_init)
3020 {
3021 page_bars.push(bar);
3022 seen_timestamps.insert(*start_time);
3023 }
3024 }
3025
3026 total_bars += page_bars.len();
3029 pages.push(page_bars);
3030
3031 if let Some(limit_val) = limit
3033 && total_bars >= limit_val as usize
3034 {
3035 break;
3036 }
3037
3038 let Some(earliest_bar_time) = earliest_ts else {
3041 break;
3042 };
3043 if let Some(start_val) = start_ms
3044 && earliest_bar_time <= start_val
3045 {
3046 break;
3047 }
3048
3049 current_end = Some(earliest_bar_time - 1);
3050
3051 if page_count > 100 {
3053 break;
3054 }
3055 }
3056
3057 let mut all_bars: Vec<Bar> = Vec::with_capacity(total_bars);
3059 for page in pages.into_iter().rev() {
3060 all_bars.extend(page);
3061 }
3062
3063 if let Some(limit_val) = limit {
3065 let limit_usize = limit_val as usize;
3066 if all_bars.len() > limit_usize {
3067 let start_idx = all_bars.len() - limit_usize;
3068 return Ok(all_bars[start_idx..].to_vec());
3069 }
3070 }
3071
3072 Ok(all_bars)
3073 }
3074
3075 pub async fn request_fee_rates(
3087 &self,
3088 product_type: BybitProductType,
3089 symbol: Option<String>,
3090 base_coin: Option<String>,
3091 ) -> anyhow::Result<Vec<BybitFeeRate>> {
3092 let params = BybitFeeRateParams {
3093 category: product_type,
3094 symbol,
3095 base_coin,
3096 };
3097
3098 let response = self.inner.get_fee_rate(¶ms).await?;
3099 Ok(response.result.list)
3100 }
3101
3102 pub async fn request_account_state(
3114 &self,
3115 account_type: BybitAccountType,
3116 account_id: AccountId,
3117 ) -> anyhow::Result<AccountState> {
3118 let params = BybitWalletBalanceParams {
3119 account_type,
3120 coin: None,
3121 };
3122
3123 let response = self.inner.get_wallet_balance(¶ms).await?;
3124 let ts_init = self.generate_ts_init();
3125
3126 let wallet_balance = response
3128 .result
3129 .list
3130 .first()
3131 .ok_or_else(|| anyhow::anyhow!("No wallet balance found in response"))?;
3132
3133 parse_account_state(wallet_balance, account_id, ts_init)
3134 }
3135
3136 #[allow(clippy::too_many_arguments)]
3147 pub async fn request_order_status_reports(
3148 &self,
3149 account_id: AccountId,
3150 product_type: BybitProductType,
3151 instrument_id: Option<InstrumentId>,
3152 open_only: bool,
3153 start: Option<DateTime<Utc>>,
3154 end: Option<DateTime<Utc>>,
3155 limit: Option<u32>,
3156 ) -> anyhow::Result<Vec<OrderStatusReport>> {
3157 let symbol_param = if let Some(id) = instrument_id.as_ref() {
3159 let symbol_str = id.symbol.as_str();
3160 if symbol_str.is_empty() {
3161 None
3162 } else {
3163 Some(BybitSymbol::new(symbol_str)?.raw_symbol().to_string())
3164 }
3165 } else {
3166 None
3167 };
3168
3169 let settle_coins_to_query: Vec<Option<String>> =
3172 if product_type == BybitProductType::Linear && symbol_param.is_none() {
3173 vec![Some("USDT".to_string()), Some("USDC".to_string())]
3174 } else {
3175 match product_type {
3176 BybitProductType::Inverse => vec![None],
3177 _ => vec![None],
3178 }
3179 };
3180
3181 let mut all_collected_orders = Vec::new();
3182 let mut total_collected_across_coins = 0;
3183
3184 for settle_coin in settle_coins_to_query {
3185 let remaining_limit = if let Some(limit) = limit {
3186 let remaining = (limit as usize).saturating_sub(total_collected_across_coins);
3187 if remaining == 0 {
3188 break;
3189 }
3190 Some(remaining as u32)
3191 } else {
3192 None
3193 };
3194
3195 let orders_for_coin = if open_only {
3196 let mut all_orders = Vec::new();
3197 let mut cursor: Option<String> = None;
3198 let mut total_orders = 0;
3199
3200 loop {
3201 let remaining = if let Some(limit) = remaining_limit {
3202 (limit as usize).saturating_sub(total_orders)
3203 } else {
3204 usize::MAX
3205 };
3206
3207 if remaining == 0 {
3208 break;
3209 }
3210
3211 let page_limit = std::cmp::min(remaining, 50);
3213
3214 let mut p = BybitOpenOrdersParamsBuilder::default();
3215 p.category(product_type);
3216 if let Some(symbol) = symbol_param.clone() {
3217 p.symbol(symbol);
3218 }
3219 if let Some(coin) = settle_coin.clone() {
3220 p.settle_coin(coin);
3221 }
3222 p.limit(page_limit as u32);
3223 if let Some(c) = cursor {
3224 p.cursor(c);
3225 }
3226 let params = p.build().map_err(|e| anyhow::anyhow!(e))?;
3227 let response: BybitOpenOrdersResponse = self
3228 .inner
3229 .send_request(Method::GET, "/v5/order/realtime", Some(¶ms), None, true)
3230 .await?;
3231
3232 total_orders += response.result.list.len();
3233 all_orders.extend(response.result.list);
3234
3235 cursor = response.result.next_page_cursor;
3236 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3237 break;
3238 }
3239 }
3240
3241 all_orders
3242 } else {
3243 let mut all_orders = Vec::new();
3246 let mut open_orders = Vec::new();
3247 let mut cursor: Option<String> = None;
3248 let mut total_open_orders = 0;
3249
3250 loop {
3251 let remaining = if let Some(limit) = remaining_limit {
3252 (limit as usize).saturating_sub(total_open_orders)
3253 } else {
3254 usize::MAX
3255 };
3256
3257 if remaining == 0 {
3258 break;
3259 }
3260
3261 let page_limit = std::cmp::min(remaining, 50);
3263
3264 let mut open_params = BybitOpenOrdersParamsBuilder::default();
3265 open_params.category(product_type);
3266 if let Some(symbol) = symbol_param.clone() {
3267 open_params.symbol(symbol);
3268 }
3269 if let Some(coin) = settle_coin.clone() {
3270 open_params.settle_coin(coin);
3271 }
3272 open_params.limit(page_limit as u32);
3273 if let Some(c) = cursor {
3274 open_params.cursor(c);
3275 }
3276 let open_params = open_params.build().map_err(|e| anyhow::anyhow!(e))?;
3277 let open_response: BybitOpenOrdersResponse = self
3278 .inner
3279 .send_request(
3280 Method::GET,
3281 "/v5/order/realtime",
3282 Some(&open_params),
3283 None,
3284 true,
3285 )
3286 .await?;
3287
3288 total_open_orders += open_response.result.list.len();
3289 open_orders.extend(open_response.result.list);
3290
3291 cursor = open_response.result.next_page_cursor;
3292 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3293 break;
3294 }
3295 }
3296
3297 let seen_order_ids: AHashSet<Ustr> =
3298 open_orders.iter().map(|o| o.order_id).collect();
3299
3300 all_orders.extend(open_orders);
3301
3302 let mut cursor: Option<String> = None;
3303 let mut total_history_orders = 0;
3304
3305 loop {
3306 let total_orders = total_open_orders + total_history_orders;
3307 let remaining = if let Some(limit) = remaining_limit {
3308 (limit as usize).saturating_sub(total_orders)
3309 } else {
3310 usize::MAX
3311 };
3312
3313 if remaining == 0 {
3314 break;
3315 }
3316
3317 let page_limit = std::cmp::min(remaining, 50);
3319
3320 let mut history_params = BybitOrderHistoryParamsBuilder::default();
3321 history_params.category(product_type);
3322 if let Some(symbol) = symbol_param.clone() {
3323 history_params.symbol(symbol);
3324 }
3325 if let Some(coin) = settle_coin.clone() {
3326 history_params.settle_coin(coin);
3327 }
3328 if let Some(start) = start {
3329 history_params.start_time(start.timestamp_millis());
3330 }
3331 if let Some(end) = end {
3332 history_params.end_time(end.timestamp_millis());
3333 }
3334 history_params.limit(page_limit as u32);
3335 if let Some(c) = cursor {
3336 history_params.cursor(c);
3337 }
3338 let history_params = history_params.build().map_err(|e| anyhow::anyhow!(e))?;
3339 let history_response: BybitOrderHistoryResponse = self
3340 .inner
3341 .send_request(
3342 Method::GET,
3343 "/v5/order/history",
3344 Some(&history_params),
3345 None,
3346 true,
3347 )
3348 .await?;
3349
3350 for order in history_response.result.list {
3352 if !seen_order_ids.contains(&order.order_id) {
3353 all_orders.push(order);
3354 total_history_orders += 1;
3355 }
3356 }
3357
3358 cursor = history_response.result.next_page_cursor;
3359 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3360 break;
3361 }
3362 }
3363
3364 all_orders
3365 };
3366
3367 total_collected_across_coins += orders_for_coin.len();
3368 all_collected_orders.extend(orders_for_coin);
3369 }
3370
3371 let ts_init = self.generate_ts_init();
3372
3373 let mut reports = Vec::new();
3374 for order in all_collected_orders {
3375 if let Some(ref instrument_id) = instrument_id {
3376 let instrument = self.instrument_from_cache(&instrument_id.symbol)?;
3377 if let Ok(report) =
3378 parse_order_status_report(&order, &instrument, account_id, ts_init)
3379 {
3380 reports.push(report);
3381 }
3382 } else {
3383 if !order.symbol.is_empty() {
3386 let symbol_with_product =
3387 Symbol::from_ustr_unchecked(make_bybit_symbol(order.symbol, product_type));
3388
3389 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3390 tracing::debug!(
3391 symbol = %order.symbol,
3392 full_symbol = %symbol_with_product,
3393 "Skipping order report for instrument not in cache"
3394 );
3395 continue;
3396 };
3397
3398 match parse_order_status_report(&order, &instrument, account_id, ts_init) {
3399 Ok(report) => reports.push(report),
3400 Err(e) => {
3401 tracing::error!("Failed to parse order status report: {e}");
3402 }
3403 }
3404 }
3405 }
3406 }
3407
3408 Ok(reports)
3409 }
3410
3411 pub async fn request_fill_reports(
3423 &self,
3424 account_id: AccountId,
3425 product_type: BybitProductType,
3426 instrument_id: Option<InstrumentId>,
3427 start: Option<i64>,
3428 end: Option<i64>,
3429 limit: Option<u32>,
3430 ) -> anyhow::Result<Vec<FillReport>> {
3431 let symbol = if let Some(id) = instrument_id {
3433 let bybit_symbol = BybitSymbol::new(id.symbol.as_str())?;
3434 Some(bybit_symbol.raw_symbol().to_string())
3435 } else {
3436 None
3437 };
3438
3439 let mut all_executions = Vec::new();
3441 let mut cursor: Option<String> = None;
3442 let mut total_executions = 0;
3443
3444 loop {
3445 let remaining = if let Some(limit) = limit {
3447 (limit as usize).saturating_sub(total_executions)
3448 } else {
3449 usize::MAX
3450 };
3451
3452 if remaining == 0 {
3454 break;
3455 }
3456
3457 let page_limit = std::cmp::min(remaining, 100);
3459
3460 let params = BybitTradeHistoryParams {
3461 category: product_type,
3462 symbol: symbol.clone(),
3463 base_coin: None,
3464 order_id: None,
3465 order_link_id: None,
3466 start_time: start,
3467 end_time: end,
3468 exec_type: None,
3469 limit: Some(page_limit as u32),
3470 cursor: cursor.clone(),
3471 };
3472
3473 let response = self.inner.get_trade_history(¶ms).await?;
3474 let list_len = response.result.list.len();
3475 all_executions.extend(response.result.list);
3476 total_executions += list_len;
3477
3478 cursor = response.result.next_page_cursor;
3479 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3480 break;
3481 }
3482 }
3483
3484 let ts_init = self.generate_ts_init();
3485 let mut reports = Vec::new();
3486
3487 for execution in all_executions {
3488 let symbol_with_product =
3491 Symbol::from_ustr_unchecked(make_bybit_symbol(execution.symbol, product_type));
3492
3493 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3494 tracing::debug!(
3495 symbol = %execution.symbol,
3496 full_symbol = %symbol_with_product,
3497 "Skipping fill report for instrument not in cache"
3498 );
3499 continue;
3500 };
3501
3502 match parse_fill_report(&execution, account_id, &instrument, ts_init) {
3503 Ok(report) => reports.push(report),
3504 Err(e) => {
3505 tracing::error!("Failed to parse fill report: {e}");
3506 }
3507 }
3508 }
3509
3510 Ok(reports)
3511 }
3512
3513 pub async fn request_position_status_reports(
3525 &self,
3526 account_id: AccountId,
3527 product_type: BybitProductType,
3528 instrument_id: Option<InstrumentId>,
3529 ) -> anyhow::Result<Vec<PositionStatusReport>> {
3530 if product_type == BybitProductType::Spot {
3532 if self.use_spot_position_reports.load(Ordering::Relaxed) {
3533 return self
3534 .generate_spot_position_reports_from_wallet(account_id, instrument_id)
3535 .await;
3536 } else {
3537 return Ok(Vec::new());
3539 }
3540 }
3541
3542 let ts_init = self.generate_ts_init();
3543 let mut reports = Vec::new();
3544
3545 let symbol = if let Some(id) = instrument_id {
3547 let symbol_str = id.symbol.as_str();
3548 if symbol_str.is_empty() {
3549 anyhow::bail!("InstrumentId symbol is empty");
3550 }
3551 let bybit_symbol = BybitSymbol::new(symbol_str)?;
3552 Some(bybit_symbol.raw_symbol().to_string())
3553 } else {
3554 None
3555 };
3556
3557 if product_type == BybitProductType::Linear && symbol.is_none() {
3560 for settle_coin in ["USDT", "USDC"] {
3562 let mut cursor: Option<String> = None;
3563
3564 loop {
3565 let params = BybitPositionListParams {
3566 category: product_type,
3567 symbol: None,
3568 base_coin: None,
3569 settle_coin: Some(settle_coin.to_string()),
3570 limit: Some(200), cursor: cursor.clone(),
3572 };
3573
3574 let response = self.inner.get_positions(¶ms).await?;
3575
3576 for position in response.result.list {
3577 if position.symbol.is_empty() {
3578 continue;
3579 }
3580
3581 let symbol_with_product = Symbol::new(format!(
3582 "{}{}",
3583 position.symbol.as_str(),
3584 product_type.suffix()
3585 ));
3586
3587 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product)
3588 else {
3589 tracing::debug!(
3590 symbol = %position.symbol,
3591 full_symbol = %symbol_with_product,
3592 "Skipping position report for instrument not in cache"
3593 );
3594 continue;
3595 };
3596
3597 match parse_position_status_report(
3598 &position,
3599 account_id,
3600 &instrument,
3601 ts_init,
3602 ) {
3603 Ok(report) => reports.push(report),
3604 Err(e) => {
3605 tracing::error!("Failed to parse position status report: {e}");
3606 }
3607 }
3608 }
3609
3610 cursor = response.result.next_page_cursor;
3611 if cursor.as_ref().is_none_or(|c| c.is_empty()) {
3612 break;
3613 }
3614 }
3615 }
3616 } else {
3617 let mut cursor: Option<String> = None;
3619
3620 loop {
3621 let params = BybitPositionListParams {
3622 category: product_type,
3623 symbol: symbol.clone(),
3624 base_coin: None,
3625 settle_coin: None,
3626 limit: Some(200), cursor: cursor.clone(),
3628 };
3629
3630 let response = self.inner.get_positions(¶ms).await?;
3631
3632 for position in response.result.list {
3633 if position.symbol.is_empty() {
3634 continue;
3635 }
3636
3637 let symbol_with_product = Symbol::new(format!(
3638 "{}{}",
3639 position.symbol.as_str(),
3640 product_type.suffix()
3641 ));
3642
3643 let Ok(instrument) = self.instrument_from_cache(&symbol_with_product) else {
3644 tracing::debug!(
3645 symbol = %position.symbol,
3646 full_symbol = %symbol_with_product,
3647 "Skipping position report for instrument not in cache"
3648 );
3649 continue;
3650 };
3651
3652 match parse_position_status_report(&position, account_id, &instrument, ts_init)
3653 {
3654 Ok(report) => reports.push(report),
3655 Err(e) => {
3656 tracing::error!("Failed to parse position status report: {e}");
3657 }
3658 }
3659 }
3660
3661 cursor = response.result.next_page_cursor;
3662 if cursor.is_none() || cursor.as_ref().is_none_or(|c| c.is_empty()) {
3663 break;
3664 }
3665 }
3666 }
3667
3668 Ok(reports)
3669 }
3670}
3671
3672#[cfg(test)]
3673mod tests {
3674 use rstest::rstest;
3675
3676 use super::*;
3677
3678 #[rstest]
3679 fn test_client_creation() {
3680 let client = BybitHttpClient::new(None, Some(60), None, None, None, None, None);
3681 assert!(client.is_ok());
3682
3683 let client = client.unwrap();
3684 assert!(client.base_url().contains("bybit.com"));
3685 assert!(client.credential().is_none());
3686 }
3687
3688 #[rstest]
3689 fn test_client_with_credentials() {
3690 let client = BybitHttpClient::with_credentials(
3691 "test_key".to_string(),
3692 "test_secret".to_string(),
3693 Some("https://api-testnet.bybit.com".to_string()),
3694 Some(60),
3695 None,
3696 None,
3697 None,
3698 None,
3699 None,
3700 );
3701 assert!(client.is_ok());
3702
3703 let client = client.unwrap();
3704 assert!(client.credential().is_some());
3705 }
3706
3707 #[rstest]
3708 fn test_build_path_with_params() {
3709 #[derive(Serialize)]
3710 struct TestParams {
3711 category: String,
3712 symbol: String,
3713 }
3714
3715 let params = TestParams {
3716 category: "linear".to_string(),
3717 symbol: "BTCUSDT".to_string(),
3718 };
3719
3720 let path = BybitRawHttpClient::build_path("/v5/market/test", ¶ms);
3721 assert!(path.is_ok());
3722 assert!(path.unwrap().contains("category=linear"));
3723 }
3724
3725 #[rstest]
3726 fn test_build_path_without_params() {
3727 let params = ();
3728 let path = BybitRawHttpClient::build_path("/v5/market/time", ¶ms);
3729 assert!(path.is_ok());
3730 assert_eq!(path.unwrap(), "/v5/market/time");
3731 }
3732
3733 #[rstest]
3734 fn test_params_serialization_matches_build_path() {
3735 #[derive(Serialize)]
3737 struct TestParams {
3738 category: String,
3739 limit: u32,
3740 }
3741
3742 let params = TestParams {
3743 category: "spot".to_string(),
3744 limit: 50,
3745 };
3746
3747 let old_path = BybitRawHttpClient::build_path("/v5/order/realtime", ¶ms).unwrap();
3749 let old_query = old_path.split('?').nth(1).unwrap_or("");
3750
3751 let new_query = serde_urlencoded::to_string(¶ms).unwrap();
3753
3754 assert_eq!(old_query, new_query);
3756 }
3757
3758 #[rstest]
3759 fn test_params_serialization_order() {
3760 #[derive(Serialize)]
3762 struct OrderParams {
3763 category: String,
3764 symbol: String,
3765 limit: u32,
3766 }
3767
3768 let params = OrderParams {
3769 category: "spot".to_string(),
3770 symbol: "BTCUSDT".to_string(),
3771 limit: 50,
3772 };
3773
3774 let query1 = serde_urlencoded::to_string(¶ms).unwrap();
3776 let query2 = serde_urlencoded::to_string(¶ms).unwrap();
3777 let query3 = serde_urlencoded::to_string(¶ms).unwrap();
3778
3779 assert_eq!(query1, query2);
3780 assert_eq!(query2, query3);
3781
3782 assert!(query1.contains("category=spot"));
3784 assert!(query1.contains("symbol=BTCUSDT"));
3785 assert!(query1.contains("limit=50"));
3786 }
3787}