1use std::{
26 collections::HashMap,
27 num::NonZeroU32,
28 sync::{
29 Arc, LazyLock, RwLock,
30 atomic::{AtomicBool, Ordering},
31 },
32};
33
34use chrono::{DateTime, Timelike, Utc};
35use dashmap::DashMap;
36use nautilus_common::cache::InstrumentLookupError;
37use nautilus_core::{
38 AtomicMap, AtomicTime, UUID4, UnixNanos,
39 consts::{NAUTILUS_TRADER, NAUTILUS_USER_AGENT},
40 env::get_or_env_var_opt,
41 time::get_atomic_clock_realtime,
42};
43use nautilus_model::{
44 data::{
45 Bar, BarType, BookOrder, FundingRateUpdate, OrderBookDelta, OrderBookDeltas, TradeTick,
46 },
47 enums::{
48 AccountType, AggregationSource, BarAggregation, BookAction, BookType, ContingencyType,
49 OrderSide, OrderType, PriceType, RecordFlag, TimeInForce, TrailingOffsetType, TriggerType,
50 },
51 events::AccountState,
52 identifiers::{AccountId, ClientOrderId, InstrumentId, OrderListId, VenueOrderId},
53 instruments::{Instrument as InstrumentTrait, InstrumentAny},
54 orderbook::OrderBook,
55 reports::{FillReport, OrderStatusReport, PositionStatusReport},
56 types::{MarginBalance, Money, Price, Quantity},
57};
58use nautilus_network::{
59 http::{HttpClient, Method, StatusCode, USER_AGENT},
60 ratelimiter::quota::Quota,
61 retry::{RetryConfig, RetryManager},
62};
63use rust_decimal::Decimal;
64use serde::{Deserialize, Serialize, de::DeserializeOwned};
65use serde_json::Value;
66use tokio_util::sync::CancellationToken;
67use ustr::Ustr;
68
69use super::{
70 error::{BitmexErrorResponse, BitmexHttpError},
71 models::{
72 BitmexApiInfo, BitmexExecution, BitmexFunding, BitmexInstrument, BitmexMargin, BitmexOrder,
73 BitmexOrderBookL2, BitmexPosition, BitmexTrade, BitmexTradeBin, BitmexWallet,
74 },
75 query::{
76 DeleteAllOrdersParams, DeleteOrderParams, GetExecutionParams, GetExecutionParamsBuilder,
77 GetFundingParams, GetFundingParamsBuilder, GetOrderBookL2Params,
78 GetOrderBookL2ParamsBuilder, GetOrderParams, GetPositionParams, GetPositionParamsBuilder,
79 GetTradeBucketedParams, GetTradeBucketedParamsBuilder, GetTradeParams,
80 GetTradeParamsBuilder, PostCancelAllAfterParams, PostOrderParams,
81 PostPositionLeverageParams, PutOrderParams,
82 },
83};
84use crate::{
85 common::{
86 consts::{BITMEX_HTTP_TESTNET_URL, BITMEX_HTTP_URL},
87 credential::{Credential, credential_env_vars},
88 enums::{
89 BitmexContingencyType, BitmexEnvironment, BitmexExecInstruction, BitmexOrderStatus,
90 BitmexOrderType, BitmexPegPriceType, BitmexSide, BitmexTimeInForce,
91 },
92 parse::{
93 bitmex_account_id, bitmex_currency_divisor, parse_account_balance,
94 parse_contracts_quantity, quantity_to_u32,
95 },
96 },
97 http::{
98 parse::{
99 InstrumentParseResult, parse_fill_report, parse_instrument_any,
100 parse_order_status_report, parse_position_report, parse_trade, parse_trade_bin,
101 },
102 query::{DeleteAllOrdersParamsBuilder, GetOrderParamsBuilder, PutOrderParamsBuilder},
103 },
104 websocket::messages::BitmexMarginMsg,
105};
106
107const BITMEX_DEFAULT_RATE_LIMIT_PER_SECOND: u32 = 10;
113const BITMEX_DEFAULT_RATE_LIMIT_PER_MINUTE_AUTHENTICATED: u32 = 120;
114const BITMEX_DEFAULT_RATE_LIMIT_PER_MINUTE_UNAUTHENTICATED: u32 = 30;
115const BITMEX_MAX_TABLE_COUNT: u32 = 500;
116
117const BITMEX_GLOBAL_RATE_KEY: &str = "bitmex:global";
118const BITMEX_MINUTE_RATE_KEY: &str = "bitmex:minute";
119
120static RATE_LIMIT_KEYS: LazyLock<Vec<Ustr>> = LazyLock::new(|| {
121 vec![
122 Ustr::from(BITMEX_GLOBAL_RATE_KEY),
123 Ustr::from(BITMEX_MINUTE_RATE_KEY),
124 ]
125});
126
127#[derive(Debug, Serialize, Deserialize)]
129pub struct BitmexResponse<T> {
130 pub data: Vec<T>,
132}
133
134#[derive(Debug, Clone)]
154pub struct BitmexRawHttpClient {
155 base_url: String,
156 client: HttpClient,
157 credential: Option<Credential>,
158 recv_window_ms: u64,
159 retry_manager: RetryManager<BitmexHttpError>,
160 cancellation_token: Arc<RwLock<CancellationToken>>,
161}
162
163impl Default for BitmexRawHttpClient {
164 fn default() -> Self {
165 Self::new(
166 None,
167 60,
168 3,
169 1000,
170 10_000,
171 10_000,
172 BITMEX_DEFAULT_RATE_LIMIT_PER_SECOND,
173 BITMEX_DEFAULT_RATE_LIMIT_PER_MINUTE_UNAUTHENTICATED,
174 None,
175 )
176 .expect("Failed to create default BitmexHttpInnerClient")
177 }
178}
179
180impl BitmexRawHttpClient {
181 #[expect(clippy::too_many_arguments)]
191 pub fn new(
192 base_url: Option<String>,
193 timeout_secs: u64,
194 max_retries: u32,
195 retry_delay_ms: u64,
196 retry_delay_max_ms: u64,
197 recv_window_ms: u64,
198 max_requests_per_second: u32,
199 max_requests_per_minute: u32,
200 proxy_url: Option<String>,
201 ) -> Result<Self, BitmexHttpError> {
202 let retry_config = RetryConfig {
203 max_retries,
204 initial_delay_ms: retry_delay_ms,
205 max_delay_ms: retry_delay_max_ms,
206 backoff_factor: 2.0,
207 jitter_ms: 1000,
208 operation_timeout_ms: Some(60_000),
209 immediate_first: false,
210 max_elapsed_ms: Some(180_000),
211 };
212
213 let retry_manager = RetryManager::new(retry_config);
214
215 Ok(Self {
216 base_url: base_url.unwrap_or(BITMEX_HTTP_URL.to_string()),
217 client: HttpClient::new(
218 Self::default_headers(),
219 vec![],
220 Self::rate_limiter_quotas(max_requests_per_second, max_requests_per_minute)?,
221 Some(Self::default_quota(max_requests_per_second)?),
222 Some(timeout_secs),
223 proxy_url,
224 )
225 .map_err(|e| {
226 BitmexHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
227 })?,
228 credential: None,
229 recv_window_ms,
230 retry_manager,
231 cancellation_token: Arc::new(RwLock::new(CancellationToken::new())),
232 })
233 }
234
235 #[expect(clippy::too_many_arguments)]
242 pub fn with_credentials(
243 api_key: String,
244 api_secret: String,
245 base_url: String,
246 timeout_secs: u64,
247 max_retries: u32,
248 retry_delay_ms: u64,
249 retry_delay_max_ms: u64,
250 recv_window_ms: u64,
251 max_requests_per_second: u32,
252 max_requests_per_minute: u32,
253 proxy_url: Option<String>,
254 ) -> Result<Self, BitmexHttpError> {
255 let retry_config = RetryConfig {
256 max_retries,
257 initial_delay_ms: retry_delay_ms,
258 max_delay_ms: retry_delay_max_ms,
259 backoff_factor: 2.0,
260 jitter_ms: 1000,
261 operation_timeout_ms: Some(60_000),
262 immediate_first: false,
263 max_elapsed_ms: Some(180_000),
264 };
265
266 let retry_manager = RetryManager::new(retry_config);
267
268 Ok(Self {
269 base_url,
270 client: HttpClient::new(
271 Self::default_headers(),
272 vec![],
273 Self::rate_limiter_quotas(max_requests_per_second, max_requests_per_minute)?,
274 Some(Self::default_quota(max_requests_per_second)?),
275 Some(timeout_secs),
276 proxy_url,
277 )
278 .map_err(|e| {
279 BitmexHttpError::NetworkError(format!("Failed to create HTTP client: {e}"))
280 })?,
281 credential: Some(Credential::new(api_key, api_secret)),
282 recv_window_ms,
283 retry_manager,
284 cancellation_token: Arc::new(RwLock::new(CancellationToken::new())),
285 })
286 }
287
288 fn default_headers() -> HashMap<String, String> {
289 HashMap::from([(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string())])
290 }
291
292 fn default_quota(max_requests_per_second: u32) -> Result<Quota, BitmexHttpError> {
293 let burst = NonZeroU32::new(max_requests_per_second)
294 .unwrap_or(NonZeroU32::new(BITMEX_DEFAULT_RATE_LIMIT_PER_SECOND).expect("non-zero"));
295 Quota::per_second(burst).ok_or_else(|| {
296 BitmexHttpError::ValidationError(format!(
297 "Invalid max_requests_per_second: {max_requests_per_second} exceeds maximum"
298 ))
299 })
300 }
301
302 fn rate_limiter_quotas(
303 max_requests_per_second: u32,
304 max_requests_per_minute: u32,
305 ) -> Result<Vec<(String, Quota)>, BitmexHttpError> {
306 let per_sec_quota = Self::default_quota(max_requests_per_second)?;
307 let per_min_quota =
308 Quota::per_minute(NonZeroU32::new(max_requests_per_minute).unwrap_or_else(|| {
309 NonZeroU32::new(BITMEX_DEFAULT_RATE_LIMIT_PER_MINUTE_AUTHENTICATED)
310 .expect("non-zero")
311 }));
312
313 Ok(vec![
314 (BITMEX_GLOBAL_RATE_KEY.to_string(), per_sec_quota),
315 (BITMEX_MINUTE_RATE_KEY.to_string(), per_min_quota),
316 ])
317 }
318
319 fn rate_limit_keys() -> Vec<Ustr> {
320 RATE_LIMIT_KEYS.clone()
321 }
322
323 pub fn cancel_all_requests(&self) {
329 self.cancellation_token
330 .read()
331 .expect("cancellation token lock poisoned")
332 .cancel();
333 }
334
335 pub fn reset_cancellation_token(&self) {
341 *self
342 .cancellation_token
343 .write()
344 .expect("cancellation token lock poisoned") = CancellationToken::new();
345 }
346
347 pub fn cancellation_token(&self) -> CancellationToken {
353 self.cancellation_token
354 .read()
355 .expect("cancellation token lock poisoned")
356 .clone()
357 }
358
359 fn sign_request(
360 &self,
361 method: &Method,
362 endpoint: &str,
363 body: Option<&[u8]>,
364 ) -> Result<HashMap<String, String>, BitmexHttpError> {
365 let credential = self
366 .credential
367 .as_ref()
368 .ok_or(BitmexHttpError::MissingCredentials)?;
369
370 let expires = Utc::now().timestamp() + (self.recv_window_ms / 1000) as i64;
371 let body_str = body.and_then(|b| std::str::from_utf8(b).ok()).unwrap_or("");
372
373 let full_path = if endpoint.starts_with("/api/v1") {
374 endpoint.to_string()
375 } else {
376 format!("/api/v1{endpoint}")
377 };
378
379 let signature = credential.sign(method.as_str(), &full_path, expires, body_str);
380
381 let mut headers = HashMap::new();
382 headers.insert("api-expires".to_string(), expires.to_string());
383 headers.insert("api-key".to_string(), credential.api_key().to_string());
384 headers.insert("api-signature".to_string(), signature);
385
386 if body.is_some()
388 && (*method == Method::POST || *method == Method::PUT || *method == Method::DELETE)
389 {
390 headers.insert(
391 "Content-Type".to_string(),
392 "application/x-www-form-urlencoded".to_string(),
393 );
394 }
395
396 Ok(headers)
397 }
398
399 async fn send_request<T: DeserializeOwned, P: Serialize>(
400 &self,
401 method: Method,
402 endpoint: &str,
403 params: Option<&P>,
404 body: Option<Vec<u8>>,
405 authenticate: bool,
406 ) -> Result<T, BitmexHttpError> {
407 let endpoint = endpoint.to_string();
408 let method_clone = method.clone();
409 let body_clone = body.clone();
410
411 let params_str = if method == Method::GET || method == Method::DELETE {
414 params
415 .map(serde_urlencoded::to_string)
416 .transpose()
417 .map_err(|e| {
418 BitmexHttpError::JsonError(format!("Failed to serialize params: {e}"))
419 })?
420 } else {
421 None
422 };
423
424 let full_endpoint = match params_str {
425 Some(ref query) if !query.is_empty() => format!("{endpoint}?{query}"),
426 _ => endpoint.clone(),
427 };
428
429 let url = format!("{}{}", self.base_url, full_endpoint);
430
431 let operation = || {
432 let url = url.clone();
433 let method = method_clone.clone();
434 let body = body_clone.clone();
435 let full_endpoint = full_endpoint.clone();
436
437 async move {
438 let headers = if authenticate {
439 Some(self.sign_request(&method, &full_endpoint, body.as_deref())?)
440 } else {
441 None
442 };
443
444 let rate_keys = Self::rate_limit_keys();
445 let resp = self
446 .client
447 .request_with_ustr_keys(method, url, None, headers, body, None, Some(rate_keys))
448 .await?;
449
450 if resp.status.is_success() {
451 serde_json::from_slice(&resp.body).map_err(Into::into)
452 } else if let Ok(error_resp) =
453 serde_json::from_slice::<BitmexErrorResponse>(&resp.body)
454 {
455 Err(error_resp.into())
456 } else {
457 Err(BitmexHttpError::UnexpectedStatus {
458 status: StatusCode::from_u16(resp.status.as_u16())
459 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
460 body: String::from_utf8_lossy(&resp.body).to_string(),
461 })
462 }
463 }
464 };
465
466 let should_retry = |error: &BitmexHttpError| -> bool {
483 match error {
484 BitmexHttpError::NetworkError(_) => true,
485 BitmexHttpError::UnexpectedStatus { status, .. } => {
486 status.as_u16() >= 500 || status.as_u16() == 429
487 }
488 BitmexHttpError::BitmexError {
489 error_name,
490 message,
491 } => {
492 error_name == "RateLimitError"
493 || (error_name == "HTTPError"
494 && message.to_lowercase().contains("rate limit"))
495 }
496 _ => false,
497 }
498 };
499
500 let create_error = |msg: String| -> BitmexHttpError {
501 if msg == "canceled" {
502 BitmexHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
503 } else {
504 BitmexHttpError::NetworkError(msg)
505 }
506 };
507
508 let cancel_token = self.cancellation_token();
509
510 self.retry_manager
511 .execute_with_retry_with_cancel(
512 endpoint.as_str(),
513 operation,
514 should_retry,
515 create_error,
516 &cancel_token,
517 )
518 .await
519 }
520
521 pub async fn get_instruments(
531 &self,
532 active_only: bool,
533 ) -> Result<Vec<BitmexInstrument>, BitmexHttpError> {
534 let path = if active_only {
535 "/instrument/active"
536 } else {
537 "/instrument"
538 };
539 let raw: Vec<serde_json::Value> = self
540 .send_request::<_, ()>(Method::GET, path, None, None, false)
541 .await?;
542
543 let raw_len = raw.len();
544 let mut instruments = Vec::with_capacity(raw_len);
545
546 for value in raw {
547 match serde_json::from_value::<BitmexInstrument>(value) {
548 Ok(inst) => instruments.push(inst),
549 Err(e) => {
550 log::warn!("Skipping instrument that could not be deserialized: {e}");
551 }
552 }
553 }
554
555 if raw_len > 0 && instruments.is_empty() {
556 return Err(BitmexHttpError::JsonError(format!(
557 "All {raw_len} instrument(s) failed to deserialize; venue schema may have changed"
558 )));
559 }
560
561 Ok(instruments)
562 }
563
564 pub async fn get_server_time(&self) -> Result<u64, BitmexHttpError> {
574 let response: BitmexApiInfo = self
575 .send_request::<_, ()>(Method::GET, "", None, None, false)
576 .await?;
577 Ok(response.timestamp)
578 }
579
580 pub async fn get_instrument(
591 &self,
592 symbol: &str,
593 ) -> Result<Option<BitmexInstrument>, BitmexHttpError> {
594 let path = &format!("/instrument?symbol={symbol}");
595 let instruments: Vec<BitmexInstrument> = self
596 .send_request::<_, ()>(Method::GET, path, None, None, false)
597 .await?;
598
599 Ok(instruments.into_iter().next())
600 }
601
602 pub async fn get_wallet(&self) -> Result<BitmexWallet, BitmexHttpError> {
608 let endpoint = "/user/wallet";
609 self.send_request::<_, ()>(Method::GET, endpoint, None, None, true)
610 .await
611 }
612
613 pub async fn get_margin(&self, currency: &str) -> Result<BitmexMargin, BitmexHttpError> {
619 let path = format!("/user/margin?currency={currency}");
620 self.send_request::<_, ()>(Method::GET, &path, None, None, true)
621 .await
622 }
623
624 pub async fn get_all_margins(&self) -> Result<Vec<BitmexMargin>, BitmexHttpError> {
630 self.send_request::<_, ()>(Method::GET, "/user/margin?currency=all", None, None, true)
631 .await
632 }
633
634 pub async fn get_trades(
640 &self,
641 params: GetTradeParams,
642 ) -> Result<Vec<BitmexTrade>, BitmexHttpError> {
643 self.send_request(Method::GET, "/trade", Some(¶ms), None, false)
644 .await
645 }
646
647 pub async fn get_trade_bucketed(
653 &self,
654 params: GetTradeBucketedParams,
655 ) -> Result<Vec<BitmexTradeBin>, BitmexHttpError> {
656 self.send_request(Method::GET, "/trade/bucketed", Some(¶ms), None, false)
657 .await
658 }
659
660 pub async fn get_order_book_l2(
666 &self,
667 params: GetOrderBookL2Params,
668 ) -> Result<Vec<BitmexOrderBookL2>, BitmexHttpError> {
669 self.send_request(Method::GET, "/orderBook/L2", Some(¶ms), None, false)
670 .await
671 }
672
673 pub async fn get_funding(
679 &self,
680 params: GetFundingParams,
681 ) -> Result<Vec<BitmexFunding>, BitmexHttpError> {
682 self.send_request(Method::GET, "/funding", Some(¶ms), None, false)
683 .await
684 }
685
686 pub async fn get_orders(
692 &self,
693 params: GetOrderParams,
694 ) -> Result<Vec<BitmexOrder>, BitmexHttpError> {
695 self.send_request(Method::GET, "/order", Some(¶ms), None, true)
696 .await
697 }
698
699 pub async fn place_order(&self, params: PostOrderParams) -> Result<Value, BitmexHttpError> {
705 self.place_order_response(params).await
706 }
707
708 async fn place_order_response<T: DeserializeOwned>(
709 &self,
710 params: PostOrderParams,
711 ) -> Result<T, BitmexHttpError> {
712 let body = serde_urlencoded::to_string(¶ms)
714 .map_err(|e| {
715 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
716 })?
717 .into_bytes();
718 let path = "/order";
719 self.send_request::<_, ()>(Method::POST, path, None, Some(body), true)
720 .await
721 }
722
723 pub async fn cancel_orders(&self, params: DeleteOrderParams) -> Result<Value, BitmexHttpError> {
729 self.cancel_orders_response(params).await
730 }
731
732 async fn cancel_orders_response<T: DeserializeOwned>(
733 &self,
734 params: DeleteOrderParams,
735 ) -> Result<T, BitmexHttpError> {
736 let body = serde_urlencoded::to_string(¶ms)
738 .map_err(|e| {
739 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
740 })?
741 .into_bytes();
742 let path = "/order";
743 self.send_request::<_, ()>(Method::DELETE, path, None, Some(body), true)
744 .await
745 }
746
747 pub async fn amend_order(&self, params: PutOrderParams) -> Result<Value, BitmexHttpError> {
753 self.amend_order_response(params).await
754 }
755
756 async fn amend_order_response<T: DeserializeOwned>(
757 &self,
758 params: PutOrderParams,
759 ) -> Result<T, BitmexHttpError> {
760 let body = serde_urlencoded::to_string(¶ms)
762 .map_err(|e| {
763 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
764 })?
765 .into_bytes();
766 let path = "/order";
767 self.send_request::<_, ()>(Method::PUT, path, None, Some(body), true)
768 .await
769 }
770
771 pub async fn cancel_all_orders(
781 &self,
782 params: DeleteAllOrdersParams,
783 ) -> Result<Value, BitmexHttpError> {
784 self.cancel_all_orders_response(params).await
785 }
786
787 async fn cancel_all_orders_response<T: DeserializeOwned>(
788 &self,
789 params: DeleteAllOrdersParams,
790 ) -> Result<T, BitmexHttpError> {
791 self.send_request(Method::DELETE, "/order/all", Some(¶ms), None, true)
792 .await
793 }
794
795 pub async fn cancel_all_after(
807 &self,
808 params: PostCancelAllAfterParams,
809 ) -> Result<Value, BitmexHttpError> {
810 let body = serde_urlencoded::to_string(¶ms)
811 .map_err(|e| {
812 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
813 })?
814 .into_bytes();
815 self.send_request::<_, ()>(
816 Method::POST,
817 "/order/cancelAllAfter",
818 None,
819 Some(body),
820 true,
821 )
822 .await
823 }
824
825 pub async fn get_executions(
831 &self,
832 params: GetExecutionParams,
833 ) -> Result<Vec<BitmexExecution>, BitmexHttpError> {
834 let query = serde_urlencoded::to_string(¶ms).map_err(|e| {
835 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
836 })?;
837 let path = format!("/execution/tradeHistory?{query}");
838 self.send_request::<_, ()>(Method::GET, &path, None, None, true)
839 .await
840 }
841
842 pub async fn get_positions(
848 &self,
849 params: GetPositionParams,
850 ) -> Result<Vec<BitmexPosition>, BitmexHttpError> {
851 self.send_request(Method::GET, "/position", Some(¶ms), None, true)
852 .await
853 }
854
855 pub async fn update_position_leverage(
861 &self,
862 params: PostPositionLeverageParams,
863 ) -> Result<BitmexPosition, BitmexHttpError> {
864 let body = serde_urlencoded::to_string(¶ms)
866 .map_err(|e| {
867 BitmexHttpError::ValidationError(format!("Failed to serialize parameters: {e}"))
868 })?
869 .into_bytes();
870 let path = "/position/leverage";
871 self.send_request::<_, ()>(Method::POST, path, None, Some(body), true)
872 .await
873 }
874}
875
876#[derive(Debug)]
881#[cfg_attr(
882 feature = "python",
883 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", from_py_object)
884)]
885#[cfg_attr(
886 feature = "python",
887 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")
888)]
889pub struct BitmexHttpClient {
890 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
891 pub(crate) order_type_cache: Arc<DashMap<ClientOrderId, OrderType>>,
892 clock: &'static AtomicTime,
893 inner: Arc<BitmexRawHttpClient>,
894 cache_initialized: AtomicBool,
895}
896
897impl Clone for BitmexHttpClient {
898 fn clone(&self) -> Self {
899 let cache_initialized = AtomicBool::new(false);
900
901 let is_initialized = self.cache_initialized.load(Ordering::Acquire);
902 if is_initialized {
903 cache_initialized.store(true, Ordering::Release);
904 }
905
906 Self {
907 inner: self.inner.clone(),
908 instruments_cache: self.instruments_cache.clone(),
909 order_type_cache: self.order_type_cache.clone(),
910 cache_initialized,
911 clock: self.clock,
912 }
913 }
914}
915
916impl Default for BitmexHttpClient {
917 fn default() -> Self {
918 Self::new(
919 None,
920 None,
921 None,
922 BitmexEnvironment::Mainnet,
923 60,
924 3,
925 1_000,
926 10_000,
927 10_000,
928 BITMEX_DEFAULT_RATE_LIMIT_PER_SECOND,
929 BITMEX_DEFAULT_RATE_LIMIT_PER_MINUTE_UNAUTHENTICATED,
930 None,
931 )
932 .expect("Failed to create default BitmexHttpClient")
933 }
934}
935
936impl BitmexHttpClient {
937 #[expect(clippy::too_many_arguments)]
943 pub fn new(
944 base_url: Option<String>,
945 api_key: Option<String>,
946 api_secret: Option<String>,
947 environment: BitmexEnvironment,
948 timeout_secs: u64,
949 max_retries: u32,
950 retry_delay_ms: u64,
951 retry_delay_max_ms: u64,
952 recv_window_ms: u64,
953 max_requests_per_second: u32,
954 max_requests_per_minute: u32,
955 proxy_url: Option<String>,
956 ) -> Result<Self, BitmexHttpError> {
957 let url = base_url.unwrap_or_else(|| match environment {
959 BitmexEnvironment::Testnet => BITMEX_HTTP_TESTNET_URL.to_string(),
960 BitmexEnvironment::Mainnet => BITMEX_HTTP_URL.to_string(),
961 });
962
963 let (key_var, secret_var) = credential_env_vars(environment);
964 let api_key = get_or_env_var_opt(api_key, key_var);
965 let api_secret = get_or_env_var_opt(api_secret, secret_var);
966
967 let inner = match (api_key, api_secret) {
968 (Some(key), Some(secret)) => BitmexRawHttpClient::with_credentials(
969 key,
970 secret,
971 url,
972 timeout_secs,
973 max_retries,
974 retry_delay_ms,
975 retry_delay_max_ms,
976 recv_window_ms,
977 max_requests_per_second,
978 max_requests_per_minute,
979 proxy_url,
980 )?,
981 (Some(_), None) | (None, Some(_)) => {
982 return Err(BitmexHttpError::ValidationError(
983 "Both api_key and api_secret must be provided, or neither".to_string(),
984 ));
985 }
986 (None, None) => BitmexRawHttpClient::new(
987 Some(url),
988 timeout_secs,
989 max_retries,
990 retry_delay_ms,
991 retry_delay_max_ms,
992 recv_window_ms,
993 max_requests_per_second,
994 max_requests_per_minute,
995 proxy_url,
996 )?,
997 };
998
999 Ok(Self {
1000 inner: Arc::new(inner),
1001 instruments_cache: Arc::new(AtomicMap::new()),
1002 order_type_cache: Arc::new(DashMap::new()),
1003 cache_initialized: AtomicBool::new(false),
1004 clock: get_atomic_clock_realtime(),
1005 })
1006 }
1007
1008 pub fn from_env() -> anyhow::Result<Self> {
1015 Self::with_credentials(
1016 None, None, None, 60, 3, 1_000, 10_000, 10_000, 10, 120, None,
1017 )
1018 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))
1019 }
1020
1021 #[expect(clippy::too_many_arguments)]
1031 pub fn with_credentials(
1032 api_key: Option<String>,
1033 api_secret: Option<String>,
1034 base_url: Option<String>,
1035 timeout_secs: u64,
1036 max_retries: u32,
1037 retry_delay_ms: u64,
1038 retry_delay_max_ms: u64,
1039 recv_window_ms: u64,
1040 max_requests_per_second: u32,
1041 max_requests_per_minute: u32,
1042 proxy_url: Option<String>,
1043 ) -> anyhow::Result<Self> {
1044 let environment = if base_url.as_ref().is_some_and(|url| url.contains("testnet")) {
1046 BitmexEnvironment::Testnet
1047 } else {
1048 BitmexEnvironment::Mainnet
1049 };
1050
1051 let (key_var, secret_var) = credential_env_vars(environment);
1052
1053 let api_key = get_or_env_var_opt(api_key, key_var);
1054 let api_secret = get_or_env_var_opt(api_secret, secret_var);
1055
1056 if api_key.is_some() && api_secret.is_none() {
1058 anyhow::bail!("{secret_var} is required when {key_var} is provided");
1059 }
1060
1061 if api_key.is_none() && api_secret.is_some() {
1062 anyhow::bail!("{key_var} is required when {secret_var} is provided");
1063 }
1064
1065 Self::new(
1066 base_url,
1067 api_key,
1068 api_secret,
1069 environment,
1070 timeout_secs,
1071 max_retries,
1072 retry_delay_ms,
1073 retry_delay_max_ms,
1074 recv_window_ms,
1075 max_requests_per_second,
1076 max_requests_per_minute,
1077 proxy_url,
1078 )
1079 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))
1080 }
1081
1082 #[must_use]
1084 pub fn base_url(&self) -> &str {
1085 self.inner.base_url.as_str()
1086 }
1087
1088 #[must_use]
1090 pub fn api_key(&self) -> Option<&str> {
1091 self.inner.credential.as_ref().map(|c| c.api_key())
1092 }
1093
1094 #[must_use]
1096 pub fn api_key_masked(&self) -> Option<String> {
1097 self.inner.credential.as_ref().map(|c| c.api_key_masked())
1098 }
1099
1100 pub async fn get_server_time(&self) -> Result<u64, BitmexHttpError> {
1108 self.inner.get_server_time().await
1109 }
1110
1111 pub async fn cancel_all_after(&self, timeout_ms: u64) -> anyhow::Result<()> {
1119 let params = PostCancelAllAfterParams {
1120 timeout: timeout_ms,
1121 };
1122 self.inner.cancel_all_after(params).await?;
1123 Ok(())
1124 }
1125
1126 fn generate_ts_init(&self) -> UnixNanos {
1128 self.clock.get_time_ns()
1129 }
1130
1131 fn is_contingent_order(contingency_type: ContingencyType) -> bool {
1133 matches!(
1134 contingency_type,
1135 ContingencyType::Oco | ContingencyType::Oto | ContingencyType::Ouo
1136 )
1137 }
1138
1139 fn is_parent_contingency(contingency_type: ContingencyType) -> bool {
1141 matches!(
1142 contingency_type,
1143 ContingencyType::Oco | ContingencyType::Oto
1144 )
1145 }
1146
1147 fn populate_linked_order_ids(reports: &mut [OrderStatusReport]) {
1149 let mut order_list_groups: HashMap<OrderListId, Vec<ClientOrderId>> = HashMap::new();
1150 let mut order_list_parents: HashMap<OrderListId, ClientOrderId> = HashMap::new();
1151 let mut prefix_groups: HashMap<String, Vec<ClientOrderId>> = HashMap::new();
1152 let mut prefix_parents: HashMap<String, ClientOrderId> = HashMap::new();
1153
1154 for report in reports.iter() {
1155 let Some(client_order_id) = report.client_order_id else {
1156 continue;
1157 };
1158
1159 if let Some(order_list_id) = report.order_list_id {
1160 order_list_groups
1161 .entry(order_list_id)
1162 .or_default()
1163 .push(client_order_id);
1164
1165 if Self::is_parent_contingency(report.contingency_type) {
1166 order_list_parents
1167 .entry(order_list_id)
1168 .or_insert(client_order_id);
1169 }
1170 }
1171
1172 if let Some((base, _)) = client_order_id.as_str().rsplit_once('-')
1173 && Self::is_contingent_order(report.contingency_type)
1174 {
1175 prefix_groups
1176 .entry(base.to_owned())
1177 .or_default()
1178 .push(client_order_id);
1179
1180 if Self::is_parent_contingency(report.contingency_type) {
1181 prefix_parents
1182 .entry(base.to_owned())
1183 .or_insert(client_order_id);
1184 }
1185 }
1186 }
1187
1188 for report in reports.iter_mut() {
1189 let Some(client_order_id) = report.client_order_id else {
1190 continue;
1191 };
1192
1193 if report.linked_order_ids.is_some() {
1194 continue;
1195 }
1196
1197 if !Self::is_contingent_order(report.contingency_type) {
1199 continue;
1200 }
1201
1202 if let Some(order_list_id) = report.order_list_id
1203 && let Some(group) = order_list_groups.get(&order_list_id)
1204 {
1205 let mut linked: Vec<ClientOrderId> = group
1206 .iter()
1207 .copied()
1208 .filter(|candidate| candidate != &client_order_id)
1209 .collect();
1210
1211 if !linked.is_empty() {
1212 if let Some(parent_id) = order_list_parents.get(&order_list_id) {
1213 if client_order_id == *parent_id {
1214 report.parent_order_id = None;
1215 } else {
1216 linked.sort_by_key(|candidate| i32::from(candidate != parent_id));
1217 report.parent_order_id = Some(*parent_id);
1218 }
1219 } else {
1220 report.parent_order_id = None;
1221 }
1222
1223 log::trace!(
1224 "BitMEX linked ids sourced from order list id: client_order_id={:?}, order_list_id={:?}, contingency_type={:?}, linked_order_ids={:?}",
1225 client_order_id,
1226 order_list_id,
1227 report.contingency_type,
1228 linked,
1229 );
1230 report.linked_order_ids = Some(linked);
1231 continue;
1232 }
1233
1234 log::trace!(
1235 "BitMEX order list id group had no peers: client_order_id={:?}, order_list_id={:?}, contingency_type={:?}, order_list_group={:?}",
1236 client_order_id,
1237 order_list_id,
1238 report.contingency_type,
1239 group,
1240 );
1241 report.parent_order_id = None;
1242 } else if report.order_list_id.is_none() {
1243 report.parent_order_id = None;
1244 }
1245
1246 if let Some((base, _)) = client_order_id.as_str().rsplit_once('-')
1247 && let Some(group) = prefix_groups.get(base)
1248 {
1249 let mut linked: Vec<ClientOrderId> = group
1250 .iter()
1251 .copied()
1252 .filter(|candidate| candidate != &client_order_id)
1253 .collect();
1254
1255 if !linked.is_empty() {
1256 if let Some(parent_id) = prefix_parents.get(base) {
1257 if client_order_id == *parent_id {
1258 report.parent_order_id = None;
1259 } else {
1260 linked.sort_by_key(|candidate| i32::from(candidate != parent_id));
1261 report.parent_order_id = Some(*parent_id);
1262 }
1263 } else {
1264 report.parent_order_id = None;
1265 }
1266
1267 log::trace!(
1268 "BitMEX linked ids constructed from client order id prefix: client_order_id={:?}, contingency_type={:?}, base={}, linked_order_ids={:?}",
1269 client_order_id,
1270 report.contingency_type,
1271 base,
1272 linked,
1273 );
1274 report.linked_order_ids = Some(linked);
1275 continue;
1276 }
1277
1278 log::trace!(
1279 "BitMEX client order id prefix group had no peers: client_order_id={:?}, contingency_type={:?}, base={}, prefix_group={:?}",
1280 client_order_id,
1281 report.contingency_type,
1282 base,
1283 group,
1284 );
1285 report.parent_order_id = None;
1286 } else if client_order_id.as_str().contains('-') {
1287 report.parent_order_id = None;
1288 }
1289
1290 if report.contingency_type == ContingencyType::Oto {
1291 log::debug!(
1292 "BitMEX OTO order has no linked venue peers; reconciling as standalone: client_order_id={:?}, order_list_id={:?}",
1293 report.client_order_id,
1294 report.order_list_id,
1295 );
1296 report.contingency_type = ContingencyType::NoContingency;
1297 report.parent_order_id = None;
1298 } else if Self::is_contingent_order(report.contingency_type) {
1299 log::warn!(
1300 "BitMEX order status report missing linked ids after grouping: client_order_id={:?}, order_list_id={:?}, contingency_type={:?}",
1301 report.client_order_id,
1302 report.order_list_id,
1303 report.contingency_type,
1304 );
1305 report.contingency_type = ContingencyType::NoContingency;
1306 report.parent_order_id = None;
1307 }
1308
1309 report.linked_order_ids = None;
1310 }
1311 }
1312
1313 pub fn cancel_all_requests(&self) {
1315 self.inner.cancel_all_requests();
1316 }
1317
1318 pub fn reset_cancellation_token(&self) {
1320 self.inner.reset_cancellation_token();
1321 }
1322
1323 pub fn cancellation_token(&self) -> CancellationToken {
1325 self.inner.cancellation_token()
1326 }
1327
1328 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1332 self.instruments_cache
1333 .insert(instrument.raw_symbol().inner(), instrument);
1334 self.cache_initialized.store(true, Ordering::Release);
1335 }
1336
1337 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1341 self.instruments_cache.rcu(|m| {
1342 for inst in instruments {
1343 m.insert(inst.raw_symbol().inner(), inst.clone());
1344 }
1345 });
1346 self.cache_initialized.store(true, Ordering::Release);
1347 }
1348
1349 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1351 self.instruments_cache.get_cloned(symbol)
1352 }
1353
1354 pub async fn request_instrument(
1362 &self,
1363 instrument_id: InstrumentId,
1364 ) -> anyhow::Result<Option<InstrumentAny>> {
1365 let response = self
1366 .inner
1367 .get_instrument(instrument_id.symbol.as_str())
1368 .await?;
1369
1370 let instrument = match response {
1371 Some(instrument) => instrument,
1372 None => return Ok(None),
1373 };
1374
1375 let ts_init = self.generate_ts_init();
1376
1377 match parse_instrument_any(&instrument, ts_init) {
1378 InstrumentParseResult::Ok(inst) => Ok(Some(*inst)),
1379 InstrumentParseResult::Unsupported {
1380 symbol,
1381 instrument_type,
1382 } => {
1383 log::debug!(
1384 "Instrument {symbol} has unsupported type {instrument_type:?}, returning None"
1385 );
1386 Ok(None)
1387 }
1388 InstrumentParseResult::Inactive { symbol, state } => {
1389 log::debug!("Instrument {symbol} is inactive (state={state}), returning None");
1390 Ok(None)
1391 }
1392 InstrumentParseResult::Failed {
1393 symbol,
1394 instrument_type,
1395 error,
1396 } => {
1397 log::error!(
1398 "Failed to parse instrument {symbol} (type={instrument_type:?}): {error}"
1399 );
1400 Ok(None)
1401 }
1402 }
1403 }
1404
1405 pub async fn request_instruments(
1411 &self,
1412 active_only: bool,
1413 ) -> anyhow::Result<Vec<InstrumentAny>> {
1414 let instruments = self.inner.get_instruments(active_only).await?;
1415 let ts_init = self.generate_ts_init();
1416
1417 let mut parsed_instruments = Vec::new();
1418 let mut skipped_count = 0;
1419 let mut inactive_count = 0;
1420 let mut failed_count = 0;
1421 let total_count = instruments.len();
1422
1423 for inst in instruments {
1424 match parse_instrument_any(&inst, ts_init) {
1425 InstrumentParseResult::Ok(instrument_any) => {
1426 parsed_instruments.push(*instrument_any);
1427 }
1428 InstrumentParseResult::Unsupported {
1429 symbol,
1430 instrument_type,
1431 } => {
1432 skipped_count += 1;
1433 log::debug!(
1434 "Skipping unsupported instrument type: symbol={symbol}, type={instrument_type:?}"
1435 );
1436 }
1437 InstrumentParseResult::Inactive { symbol, state } => {
1438 inactive_count += 1;
1439 log::debug!("Skipping inactive instrument: symbol={symbol}, state={state}");
1440 }
1441 InstrumentParseResult::Failed {
1442 symbol,
1443 instrument_type,
1444 error,
1445 } => {
1446 failed_count += 1;
1447 log::error!(
1448 "Failed to parse instrument: symbol={symbol}, type={instrument_type:?}, error={error}"
1449 );
1450 }
1451 }
1452 }
1453
1454 if skipped_count > 0 {
1455 log::debug!(
1456 "Skipped {skipped_count} unsupported instrument type(s) out of {total_count} total"
1457 );
1458 }
1459
1460 if inactive_count > 0 {
1461 log::debug!(
1462 "Skipped {inactive_count} inactive instrument(s) out of {total_count} total"
1463 );
1464 }
1465
1466 if failed_count > 0 {
1467 log::error!(
1468 "Instrument parse failures: {failed_count} failed out of {total_count} total ({} successfully parsed)",
1469 parsed_instruments.len()
1470 );
1471 }
1472
1473 Ok(parsed_instruments)
1474 }
1475
1476 pub async fn get_wallet(&self) -> Result<BitmexWallet, BitmexHttpError> {
1482 let inner = self.inner.clone();
1483 inner.get_wallet().await
1484 }
1485
1486 pub async fn get_orders(
1492 &self,
1493 params: GetOrderParams,
1494 ) -> Result<Vec<BitmexOrder>, BitmexHttpError> {
1495 let inner = self.inner.clone();
1496 inner.get_orders(params).await
1497 }
1498
1499 fn instrument_from_cache(&self, symbol: Ustr) -> anyhow::Result<InstrumentAny> {
1505 self.get_instrument(&symbol).ok_or_else(|| {
1506 anyhow::anyhow!(
1507 "Instrument {symbol} not found in cache, ensure instruments loaded first"
1508 )
1509 })
1510 }
1511
1512 pub fn get_price_precision(&self, symbol: Ustr) -> anyhow::Result<u8> {
1519 self.instrument_from_cache(symbol)
1520 .map(|instrument| instrument.price_precision())
1521 }
1522
1523 pub async fn get_margin(&self, currency: &str) -> anyhow::Result<BitmexMargin> {
1529 self.inner
1530 .get_margin(currency)
1531 .await
1532 .map_err(|e| anyhow::anyhow!(e))
1533 }
1534
1535 pub async fn get_all_margins(&self) -> anyhow::Result<Vec<BitmexMargin>> {
1541 self.inner
1542 .get_all_margins()
1543 .await
1544 .map_err(|e| anyhow::anyhow!(e))
1545 }
1546
1547 pub async fn request_account_state(
1553 &self,
1554 fallback_account_id: AccountId,
1555 ) -> anyhow::Result<AccountState> {
1556 let margins = self
1557 .inner
1558 .get_all_margins()
1559 .await
1560 .map_err(|e| anyhow::anyhow!(e))?;
1561 let account_id = account_id_from_margins(&margins)?.unwrap_or(fallback_account_id);
1562
1563 let ts_init =
1564 UnixNanos::from(chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default() as u64);
1565
1566 let mut balances = Vec::with_capacity(margins.len());
1567 let mut margins_vec = Vec::new();
1568 let mut latest_timestamp: Option<chrono::DateTime<chrono::Utc>> = None;
1569
1570 for margin in margins {
1571 if let Some(ts) = margin.timestamp {
1572 latest_timestamp = Some(latest_timestamp.map_or(ts, |prev| prev.max(ts)));
1573 }
1574
1575 let margin_msg = BitmexMarginMsg {
1576 account: margin.account,
1577 currency: margin.currency,
1578 risk_limit: margin.risk_limit,
1579 amount: margin.amount,
1580 prev_realised_pnl: margin.prev_realised_pnl,
1581 gross_comm: margin.gross_comm,
1582 gross_open_cost: margin.gross_open_cost,
1583 gross_open_premium: margin.gross_open_premium,
1584 gross_exec_cost: margin.gross_exec_cost,
1585 gross_mark_value: margin.gross_mark_value,
1586 risk_value: margin.risk_value,
1587 init_margin: margin.init_margin,
1588 maint_margin: margin.maint_margin,
1589 target_excess_margin: margin.target_excess_margin,
1590 realised_pnl: margin.realised_pnl,
1591 unrealised_pnl: margin.unrealised_pnl,
1592 wallet_balance: margin.wallet_balance,
1593 margin_balance: margin.margin_balance,
1594 margin_leverage: margin.margin_leverage,
1595 margin_used_pcnt: margin.margin_used_pcnt,
1596 excess_margin: margin.excess_margin,
1597 available_margin: margin.available_margin,
1598 withdrawable_margin: margin.withdrawable_margin,
1599 maker_fee_discount: None,
1600 taker_fee_discount: None,
1601 timestamp: margin.timestamp.unwrap_or_else(chrono::Utc::now),
1602 foreign_margin_balance: None,
1603 foreign_requirement: None,
1604 };
1605
1606 let balance = parse_account_balance(&margin_msg);
1607
1608 let divisor = bitmex_currency_divisor(margin_msg.currency.as_str());
1609 let initial_dec = Decimal::from(margin_msg.init_margin.unwrap_or(0).max(0)) / divisor;
1610 let maintenance_dec =
1611 Decimal::from(margin_msg.maint_margin.unwrap_or(0).max(0)) / divisor;
1612
1613 if !initial_dec.is_zero() || !maintenance_dec.is_zero() {
1614 let currency = balance.total.currency;
1615 margins_vec.push(MarginBalance::new(
1617 Money::from_decimal(initial_dec, currency)
1618 .unwrap_or_else(|_| Money::zero(currency)),
1619 Money::from_decimal(maintenance_dec, currency)
1620 .unwrap_or_else(|_| Money::zero(currency)),
1621 None,
1622 ));
1623 }
1624
1625 balances.push(balance);
1626 }
1627
1628 if balances.is_empty() {
1629 anyhow::bail!("No margin data returned from BitMEX");
1630 }
1631
1632 let account_type = AccountType::Margin;
1633 let is_reported = true;
1634 let event_id = UUID4::new();
1635
1636 let ts_event = latest_timestamp.map_or(ts_init, |ts| {
1638 UnixNanos::from(ts.timestamp_nanos_opt().unwrap_or_default() as u64)
1639 });
1640
1641 Ok(AccountState::new(
1642 account_id,
1643 account_type,
1644 balances,
1645 margins_vec,
1646 is_reported,
1647 event_id,
1648 ts_event,
1649 ts_init,
1650 None,
1651 ))
1652 }
1653
1654 #[expect(clippy::too_many_arguments)]
1661 pub async fn submit_order(
1662 &self,
1663 instrument_id: InstrumentId,
1664 client_order_id: ClientOrderId,
1665 order_side: OrderSide,
1666 order_type: OrderType,
1667 quantity: Quantity,
1668 time_in_force: TimeInForce,
1669 price: Option<Price>,
1670 trigger_price: Option<Price>,
1671 trigger_type: Option<TriggerType>,
1672 trailing_offset: Option<f64>,
1673 trailing_offset_type: Option<TrailingOffsetType>,
1674 display_qty: Option<Quantity>,
1675 post_only: bool,
1676 reduce_only: bool,
1677 order_list_id: Option<OrderListId>,
1678 contingency_type: Option<ContingencyType>,
1679 peg_price_type: Option<BitmexPegPriceType>,
1680 peg_offset_value: Option<f64>,
1681 ) -> anyhow::Result<OrderStatusReport> {
1682 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
1683
1684 let mut params = super::query::PostOrderParamsBuilder::default();
1685 params.text(NAUTILUS_TRADER);
1686 params.symbol(instrument_id.symbol.as_str());
1687 params.cl_ord_id(client_order_id.as_str());
1688
1689 if order_side == OrderSide::NoOrderSide {
1690 anyhow::bail!("Order side must be Buy or Sell");
1691 }
1692 let side = BitmexSide::from(order_side.as_specified());
1693 params.side(side);
1694
1695 let ord_type = BitmexOrderType::try_from_order_type(order_type)?;
1696 params.ord_type(ord_type);
1697
1698 params.order_qty(quantity_to_u32(&quantity, &instrument));
1699
1700 let tif = BitmexTimeInForce::try_from_time_in_force(time_in_force)?;
1701 params.time_in_force(tif);
1702
1703 if let Some(price) = price {
1704 params.price(price.as_f64());
1705 }
1706
1707 if let Some(trigger_price) = trigger_price {
1708 params.stop_px(trigger_price.as_f64());
1709 }
1710
1711 if let Some(display_qty) = display_qty {
1712 params.display_qty(quantity_to_u32(&display_qty, &instrument));
1713 }
1714
1715 if let Some(order_list_id) = order_list_id {
1716 params.cl_ord_link_id(order_list_id.as_str());
1717 }
1718
1719 let is_trailing_stop = matches!(
1720 order_type,
1721 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
1722 );
1723
1724 if is_trailing_stop && let Some(offset) = trailing_offset {
1725 if let Some(offset_type) = trailing_offset_type
1726 && offset_type != TrailingOffsetType::Price
1727 {
1728 anyhow::bail!(
1729 "BitMEX only supports PRICE trailing offset type, was {offset_type:?}"
1730 );
1731 }
1732
1733 params.peg_price_type(BitmexPegPriceType::TrailingStopPeg);
1734
1735 let signed_offset = match order_side {
1737 OrderSide::Sell => -offset.abs(),
1738 OrderSide::Buy => offset.abs(),
1739 _ => offset,
1740 };
1741 params.peg_offset_value(signed_offset);
1742 }
1743
1744 if peg_price_type.is_none() && peg_offset_value.is_some() {
1746 anyhow::bail!("`peg_offset_value` requires `peg_price_type`");
1747 }
1748
1749 if let Some(peg_type) = peg_price_type {
1750 if order_type != OrderType::Limit {
1751 anyhow::bail!(
1752 "Pegged orders only supported for LIMIT order type, was {order_type:?}"
1753 );
1754 }
1755 params.ord_type(BitmexOrderType::Pegged);
1756 params.peg_price_type(peg_type);
1757
1758 if let Some(offset) = peg_offset_value {
1759 params.peg_offset_value(offset);
1760 }
1761 }
1762
1763 let mut exec_inst = Vec::new();
1764
1765 if post_only {
1766 exec_inst.push(BitmexExecInstruction::ParticipateDoNotInitiate);
1767 }
1768
1769 if reduce_only {
1770 exec_inst.push(BitmexExecInstruction::ReduceOnly);
1771 }
1772
1773 if (trigger_price.is_some() || is_trailing_stop)
1775 && let Some(trigger_type) = trigger_type
1776 {
1777 match trigger_type {
1778 TriggerType::LastPrice => exec_inst.push(BitmexExecInstruction::LastPrice),
1779 TriggerType::MarkPrice => exec_inst.push(BitmexExecInstruction::MarkPrice),
1780 TriggerType::IndexPrice => exec_inst.push(BitmexExecInstruction::IndexPrice),
1781 _ => {} }
1783 }
1784
1785 if !exec_inst.is_empty() {
1786 params.exec_inst(exec_inst);
1787 }
1788
1789 if let Some(contingency_type) = contingency_type {
1790 let bitmex_contingency = BitmexContingencyType::try_from(contingency_type)?;
1791 params.contingency_type(bitmex_contingency);
1792 }
1793
1794 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1795
1796 let order: BitmexOrder = self.inner.place_order_response(params).await?;
1797
1798 if order.ord_status == Some(BitmexOrderStatus::Rejected) {
1799 let reason = order
1800 .ord_rej_reason
1801 .map_or_else(|| "No reason provided".to_string(), |r| r.to_string());
1802 anyhow::bail!("Order rejected: {reason}");
1803 }
1804
1805 self.order_type_cache.insert(client_order_id, order_type);
1807
1808 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
1809 let ts_init = self.generate_ts_init();
1810
1811 parse_order_status_report(&order, &instrument, &self.order_type_cache, ts_init)
1812 }
1813
1814 pub async fn cancel_order(
1824 &self,
1825 instrument_id: InstrumentId,
1826 client_order_id: Option<ClientOrderId>,
1827 venue_order_id: Option<VenueOrderId>,
1828 ) -> anyhow::Result<OrderStatusReport> {
1829 let mut params = super::query::DeleteOrderParamsBuilder::default();
1830 params.text(NAUTILUS_TRADER);
1831
1832 if let Some(venue_order_id) = venue_order_id {
1833 params.order_id(vec![venue_order_id.as_str().to_string()]);
1834 } else if let Some(client_order_id) = client_order_id {
1835 params.cl_ord_id(vec![client_order_id.as_str().to_string()]);
1836 } else {
1837 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
1838 }
1839
1840 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1841
1842 let orders: Vec<BitmexOrder> = self.inner.cancel_orders_response(params).await?;
1843 let order = orders
1844 .into_iter()
1845 .next()
1846 .ok_or_else(|| anyhow::anyhow!("No order returned in cancel response"))?;
1847
1848 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
1849 let ts_init = self.generate_ts_init();
1850
1851 parse_order_status_report(&order, &instrument, &self.order_type_cache, ts_init)
1852 }
1853
1854 pub async fn cancel_orders(
1864 &self,
1865 instrument_id: InstrumentId,
1866 client_order_ids: Option<Vec<ClientOrderId>>,
1867 venue_order_ids: Option<Vec<VenueOrderId>>,
1868 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1869 let mut params = super::query::DeleteOrderParamsBuilder::default();
1870 params.text(NAUTILUS_TRADER);
1871
1872 if let Some(venue_order_ids) = venue_order_ids {
1875 if venue_order_ids.is_empty() {
1876 anyhow::bail!("venue_order_ids cannot be empty");
1877 }
1878 params.order_id(
1879 venue_order_ids
1880 .iter()
1881 .map(|id| id.to_string())
1882 .collect::<Vec<_>>(),
1883 );
1884 } else if let Some(client_order_ids) = client_order_ids {
1885 if client_order_ids.is_empty() {
1886 anyhow::bail!("client_order_ids cannot be empty");
1887 }
1888 params.cl_ord_id(
1889 client_order_ids
1890 .iter()
1891 .map(|id| id.to_string())
1892 .collect::<Vec<_>>(),
1893 );
1894 } else {
1895 anyhow::bail!("Either client_order_ids or venue_order_ids must be provided");
1896 }
1897
1898 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1899
1900 let orders: Vec<BitmexOrder> = self.inner.cancel_orders_response(params).await?;
1901
1902 let ts_init = self.generate_ts_init();
1903 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
1904
1905 let mut reports = Vec::new();
1906
1907 for order in orders {
1908 reports.push(parse_order_status_report(
1909 &order,
1910 &instrument,
1911 &self.order_type_cache,
1912 ts_init,
1913 )?);
1914 }
1915
1916 Self::populate_linked_order_ids(&mut reports);
1917
1918 Ok(reports)
1919 }
1920
1921 pub async fn cancel_all_orders(
1931 &self,
1932 instrument_id: InstrumentId,
1933 order_side: Option<OrderSide>,
1934 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1935 let mut params = DeleteAllOrdersParamsBuilder::default();
1936 params.text(NAUTILUS_TRADER);
1937 params.symbol(instrument_id.symbol.as_str());
1938
1939 if let Some(side) = order_side {
1940 if side == OrderSide::NoOrderSide {
1941 log::debug!("Ignoring NoOrderSide filter for cancel_all_orders on {instrument_id}",);
1942 } else {
1943 let side = BitmexSide::from(side.as_specified());
1944 params.filter(serde_json::json!({
1945 "side": side
1946 }));
1947 }
1948 }
1949
1950 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
1951
1952 let orders: Vec<BitmexOrder> = self.inner.cancel_all_orders_response(params).await?;
1953
1954 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
1955 let ts_init = self.generate_ts_init();
1956
1957 let mut reports = Vec::new();
1958
1959 for order in orders {
1960 if is_cancel_all_rejection(&order) {
1961 continue;
1962 }
1963
1964 reports.push(parse_order_status_report(
1965 &order,
1966 &instrument,
1967 &self.order_type_cache,
1968 ts_init,
1969 )?);
1970 }
1971
1972 Self::populate_linked_order_ids(&mut reports);
1973
1974 Ok(reports)
1975 }
1976
1977 pub async fn modify_order(
1988 &self,
1989 instrument_id: InstrumentId,
1990 client_order_id: Option<ClientOrderId>,
1991 venue_order_id: Option<VenueOrderId>,
1992 quantity: Option<Quantity>,
1993 price: Option<Price>,
1994 trigger_price: Option<Price>,
1995 ) -> anyhow::Result<OrderStatusReport> {
1996 let mut params = PutOrderParamsBuilder::default();
1997 params.text(NAUTILUS_TRADER);
1998
1999 if let Some(venue_order_id) = venue_order_id {
2001 params.order_id(venue_order_id.as_str());
2002 } else if let Some(client_order_id) = client_order_id {
2003 params.orig_cl_ord_id(client_order_id.as_str());
2004 } else {
2005 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2006 }
2007
2008 if let Some(quantity) = quantity {
2009 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
2010 params.order_qty(quantity_to_u32(&quantity, &instrument));
2011 }
2012
2013 if let Some(price) = price {
2014 params.price(price.as_f64());
2015 }
2016
2017 if let Some(trigger_price) = trigger_price {
2018 params.stop_px(trigger_price.as_f64());
2019 }
2020
2021 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2022
2023 let order: BitmexOrder = self.inner.amend_order_response(params).await?;
2024
2025 if order.ord_status == Some(BitmexOrderStatus::Rejected) {
2026 let reason = order
2027 .ord_rej_reason
2028 .map_or_else(|| "No reason provided".to_string(), |r| r.to_string());
2029 anyhow::bail!("Order modification rejected: {reason}");
2030 }
2031
2032 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
2033 let ts_init = self.generate_ts_init();
2034
2035 parse_order_status_report(&order, &instrument, &self.order_type_cache, ts_init)
2036 }
2037
2038 pub async fn query_order(
2047 &self,
2048 instrument_id: InstrumentId,
2049 client_order_id: Option<ClientOrderId>,
2050 venue_order_id: Option<VenueOrderId>,
2051 ) -> anyhow::Result<Option<OrderStatusReport>> {
2052 let mut params = GetOrderParamsBuilder::default();
2053
2054 let filter_json = if let Some(client_order_id) = client_order_id {
2055 serde_json::json!({
2056 "clOrdID": client_order_id.to_string()
2057 })
2058 } else if let Some(venue_order_id) = venue_order_id {
2059 serde_json::json!({
2060 "orderID": venue_order_id.to_string()
2061 })
2062 } else {
2063 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2064 };
2065
2066 params.filter(filter_json);
2067 params.count(1); let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2070
2071 let response = self.inner.get_orders(params).await?;
2072
2073 if response.is_empty() {
2074 return Ok(None);
2075 }
2076
2077 let order = &response[0];
2078
2079 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
2080 let ts_init = self.generate_ts_init();
2081
2082 let report =
2083 parse_order_status_report(order, &instrument, &self.order_type_cache, ts_init)?;
2084
2085 Ok(Some(report))
2086 }
2087
2088 pub async fn request_order_status_report(
2097 &self,
2098 instrument_id: InstrumentId,
2099 client_order_id: Option<ClientOrderId>,
2100 venue_order_id: Option<VenueOrderId>,
2101 ) -> anyhow::Result<OrderStatusReport> {
2102 if venue_order_id.is_none() && client_order_id.is_none() {
2103 anyhow::bail!("Either venue_order_id or client_order_id must be provided");
2104 }
2105
2106 let mut params = GetOrderParamsBuilder::default();
2107 params.symbol(instrument_id.symbol.as_str());
2108
2109 if let Some(venue_order_id) = venue_order_id {
2110 params.filter(serde_json::json!({
2111 "orderID": venue_order_id.as_str()
2112 }));
2113 } else if let Some(client_order_id) = client_order_id {
2114 params.filter(serde_json::json!({
2115 "clOrdID": client_order_id.as_str()
2116 }));
2117 }
2118
2119 params.count(1i32);
2120 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2121
2122 let response = self.inner.get_orders(params).await?;
2123
2124 let order = response
2125 .into_iter()
2126 .next()
2127 .ok_or_else(|| anyhow::anyhow!("Order not found"))?;
2128
2129 let instrument = self.instrument_from_cache(instrument_id.symbol.inner())?;
2130 let ts_init = self.generate_ts_init();
2131
2132 parse_order_status_report(&order, &instrument, &self.order_type_cache, ts_init)
2133 }
2134
2135 pub async fn request_order_status_reports(
2144 &self,
2145 instrument_id: Option<InstrumentId>,
2146 open_only: bool,
2147 start: Option<DateTime<Utc>>,
2148 end: Option<DateTime<Utc>>,
2149 limit: Option<u32>,
2150 ) -> anyhow::Result<Vec<OrderStatusReport>> {
2151 if let (Some(start), Some(end)) = (start, end) {
2152 anyhow::ensure!(
2153 start < end,
2154 "Invalid time range: start={start:?} end={end:?}",
2155 );
2156 }
2157
2158 let mut params = GetOrderParamsBuilder::default();
2159
2160 if let Some(instrument_id) = &instrument_id {
2161 params.symbol(instrument_id.symbol.as_str());
2162 }
2163
2164 if open_only {
2165 params.filter(serde_json::json!({
2166 "open": true
2167 }));
2168 }
2169
2170 if let Some(start) = start {
2171 params.start_time(start);
2172 }
2173
2174 if let Some(end) = end {
2175 params.end_time(end);
2176 }
2177
2178 if let Some(limit) = limit {
2179 params.count(limit as i32);
2180 } else {
2181 params.count(500); }
2183
2184 params.reverse(true); let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2187
2188 let response = self.inner.get_orders(params).await?;
2189
2190 let ts_init = self.generate_ts_init();
2191
2192 let mut reports = Vec::new();
2193
2194 for order in response {
2195 if let Some(start) = start {
2196 match order.timestamp {
2197 Some(timestamp) if timestamp < start => continue,
2198 Some(_) => {}
2199 None => {
2200 log::debug!("Skipping order report without timestamp for bounded query");
2201 continue;
2202 }
2203 }
2204 }
2205
2206 if let Some(end) = end {
2207 match order.timestamp {
2208 Some(timestamp) if timestamp > end => continue,
2209 Some(_) => {}
2210 None => {
2211 log::debug!("Skipping order report without timestamp for bounded query");
2212 continue;
2213 }
2214 }
2215 }
2216
2217 let Some(symbol) = order.symbol else {
2219 log::warn!("Order response missing symbol, skipping");
2220 continue;
2221 };
2222
2223 let Ok(instrument) = self.instrument_from_cache(symbol) else {
2224 log::debug!("Skipping order report for instrument not in cache: symbol={symbol}");
2225 continue;
2226 };
2227
2228 match parse_order_status_report(&order, &instrument, &self.order_type_cache, ts_init) {
2229 Ok(report) => reports.push(report),
2230 Err(e) => log::error!("Failed to parse order status report: {e}"),
2231 }
2232 }
2233
2234 Self::populate_linked_order_ids(&mut reports);
2235
2236 Ok(reports)
2237 }
2238
2239 pub async fn request_trades(
2245 &self,
2246 instrument_id: InstrumentId,
2247 start: Option<DateTime<Utc>>,
2248 end: Option<DateTime<Utc>>,
2249 limit: Option<u32>,
2250 ) -> anyhow::Result<Vec<TradeTick>> {
2251 let mut params = GetTradeParamsBuilder::default();
2252 params.symbol(instrument_id.symbol.as_str());
2253
2254 if let Some(start) = start {
2255 params.start_time(start);
2256 }
2257
2258 if let Some(end) = end {
2259 params.end_time(end);
2260 }
2261
2262 if let (Some(start), Some(end)) = (start, end) {
2263 anyhow::ensure!(
2264 start < end,
2265 "Invalid time range: start={start:?} end={end:?}",
2266 );
2267 }
2268
2269 if let Some(limit) = limit {
2270 let clamped_limit = limit.min(1000);
2271 if limit > 1000 {
2272 log::warn!(
2273 "BitMEX trade request limit exceeds venue maximum; clamping: limit={limit}, clamped_limit={clamped_limit}",
2274 );
2275 }
2276 params.count(i32::try_from(clamped_limit).unwrap_or(1000));
2277 }
2278 params.reverse(false);
2279 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2280
2281 let response = self.inner.get_trades(params).await?;
2282
2283 let ts_init = self.generate_ts_init();
2284
2285 let mut parsed_trades = Vec::new();
2286
2287 for trade in response {
2288 if let Some(start) = start
2289 && trade.timestamp < start
2290 {
2291 continue;
2292 }
2293
2294 if let Some(end) = end
2295 && trade.timestamp > end
2296 {
2297 continue;
2298 }
2299
2300 let Some(instrument) = self.get_instrument(&trade.symbol) else {
2301 log::error!(
2302 "Instrument {} not found in cache, skipping trade",
2303 trade.symbol
2304 );
2305 continue;
2306 };
2307
2308 match parse_trade(&trade, &instrument, ts_init) {
2309 Ok(trade) => parsed_trades.push(trade),
2310 Err(e) => log::error!("Failed to parse trade: {e}"),
2311 }
2312 }
2313
2314 Ok(parsed_trades)
2315 }
2316
2317 pub async fn request_bars(
2324 &self,
2325 mut bar_type: BarType,
2326 start: Option<DateTime<Utc>>,
2327 end: Option<DateTime<Utc>>,
2328 limit: Option<u32>,
2329 partial: bool,
2330 ) -> anyhow::Result<Vec<Bar>> {
2331 bar_type = bar_type.standard();
2332
2333 anyhow::ensure!(
2334 bar_type.aggregation_source() == AggregationSource::External,
2335 "Only EXTERNAL aggregation bars are supported"
2336 );
2337 anyhow::ensure!(
2338 bar_type.spec().price_type == PriceType::Last,
2339 "Only LAST price type bars are supported"
2340 );
2341
2342 if let (Some(start), Some(end)) = (start, end) {
2343 anyhow::ensure!(
2344 start < end,
2345 "Invalid time range: start={start:?} end={end:?}"
2346 );
2347 }
2348
2349 let spec = bar_type.spec();
2350 let bin_size = match (spec.aggregation, spec.step.get()) {
2351 (BarAggregation::Minute, 1) => "1m",
2352 (BarAggregation::Minute, 5) => "5m",
2353 (BarAggregation::Hour, 1) => "1h",
2354 (BarAggregation::Day, 1) => "1d",
2355 _ => anyhow::bail!(
2356 "BitMEX does not support {}-{:?}-{:?} bars",
2357 spec.step.get(),
2358 spec.aggregation,
2359 spec.price_type,
2360 ),
2361 };
2362
2363 let instrument_id = bar_type.instrument_id();
2364 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2365
2366 let mut params = GetTradeBucketedParamsBuilder::default();
2367 params.symbol(instrument_id.symbol.as_str());
2368 params.bin_size(bin_size);
2369
2370 if partial {
2371 params.partial(true);
2372 }
2373
2374 if let Some(start) = start {
2375 params.start_time(start);
2376 }
2377
2378 if let Some(end) = end {
2379 params.end_time(end);
2380 }
2381
2382 if let Some(limit) = limit {
2383 let clamped_limit = limit.min(1000);
2384 if limit > 1000 {
2385 log::warn!(
2386 "BitMEX bar request limit exceeds venue maximum; clamping: limit={limit}, clamped_limit={clamped_limit}",
2387 );
2388 }
2389 params.count(i32::try_from(clamped_limit).unwrap_or(1000));
2390 }
2391 params.reverse(false);
2392 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2393
2394 let response = self.inner.get_trade_bucketed(params).await?;
2395 let ts_init = self.generate_ts_init();
2396 let mut bars = Vec::new();
2397
2398 for bin in response {
2399 if let Some(start) = start
2400 && bin.timestamp < start
2401 {
2402 continue;
2403 }
2404
2405 if let Some(end) = end
2406 && bin.timestamp > end
2407 {
2408 continue;
2409 }
2410
2411 if bin.symbol != instrument_id.symbol.inner() {
2412 log::warn!(
2413 "Skipping trade bin for unexpected symbol: symbol={}, expected={}",
2414 bin.symbol,
2415 instrument_id.symbol,
2416 );
2417 continue;
2418 }
2419
2420 match parse_trade_bin(&bin, &instrument, &bar_type, ts_init) {
2421 Ok(bar) => bars.push(bar),
2422 Err(e) => log::warn!("Failed to parse trade bin: {e}"),
2423 }
2424 }
2425
2426 Ok(bars)
2427 }
2428
2429 pub async fn request_book_snapshot(
2436 &self,
2437 instrument_id: InstrumentId,
2438 depth: Option<u32>,
2439 ) -> anyhow::Result<OrderBook> {
2440 let instrument = self.instrument_from_cache_by_id(instrument_id)?;
2441 let mut params = GetOrderBookL2ParamsBuilder::default();
2442 params.symbol(instrument_id.symbol.as_str());
2443
2444 if let Some(depth) = depth {
2445 params.depth(depth);
2446 }
2447
2448 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2449 let response = self.inner.get_order_book_l2(params).await?;
2450 let ts_init = self.generate_ts_init();
2451 let deltas = parse_order_book_l2_snapshot(&response, &instrument, instrument_id, ts_init)?;
2452
2453 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
2454 book.apply_deltas(&deltas)?;
2455 Ok(book)
2456 }
2457
2458 fn instrument_from_cache_by_id(
2459 &self,
2460 instrument_id: InstrumentId,
2461 ) -> anyhow::Result<InstrumentAny> {
2462 self.get_instrument(&instrument_id.symbol.inner())
2463 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id).into())
2464 }
2465
2466 pub async fn request_funding_rates(
2472 &self,
2473 instrument_id: InstrumentId,
2474 start: Option<DateTime<Utc>>,
2475 end: Option<DateTime<Utc>>,
2476 limit: Option<u32>,
2477 ) -> anyhow::Result<Vec<FundingRateUpdate>> {
2478 if let (Some(start), Some(end)) = (start, end) {
2479 anyhow::ensure!(
2480 start < end,
2481 "Invalid time range: start={start:?} end={end:?}",
2482 );
2483 }
2484
2485 let total_limit = limit.map(|value| value as usize);
2486 let mut offset = 0_i32;
2487 let mut rates = Vec::new();
2488
2489 loop {
2490 if total_limit.is_some_and(|limit| rates.len() >= limit) {
2491 break;
2492 }
2493
2494 let remaining = total_limit.map_or(BITMEX_MAX_TABLE_COUNT as usize, |limit| {
2495 limit.saturating_sub(rates.len())
2496 });
2497 let page_count = remaining.min(BITMEX_MAX_TABLE_COUNT as usize);
2498
2499 if page_count == 0 {
2500 break;
2501 }
2502
2503 let mut params = GetFundingParamsBuilder::default();
2504 params.symbol(instrument_id.symbol.as_str());
2505 params.count(i32::try_from(page_count).unwrap_or(BITMEX_MAX_TABLE_COUNT as i32));
2506 params.start(offset);
2507 params.reverse(false);
2508
2509 if let Some(start) = start {
2510 params.start_time(start);
2511 }
2512
2513 if let Some(end) = end {
2514 params.end_time(end);
2515 }
2516
2517 let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2518 let response = self.inner.get_funding(params).await?;
2519 let response_len = response.len();
2520
2521 if response.is_empty() {
2522 break;
2523 }
2524
2525 for raw in response {
2526 if raw.symbol != instrument_id.symbol.inner() {
2527 log::warn!(
2528 "Skipping funding rate for unexpected symbol: symbol={}, expected={}",
2529 raw.symbol,
2530 instrument_id.symbol,
2531 );
2532 continue;
2533 }
2534
2535 if let Some(start) = start
2536 && raw.timestamp < start
2537 {
2538 continue;
2539 }
2540
2541 if let Some(end) = end
2542 && raw.timestamp > end
2543 {
2544 continue;
2545 }
2546
2547 let Some(rate) = parse_funding_rate_update(&raw, instrument_id) else {
2548 continue;
2549 };
2550
2551 rates.push(rate);
2552
2553 if total_limit.is_some_and(|limit| rates.len() >= limit) {
2554 break;
2555 }
2556 }
2557
2558 if response_len < page_count {
2559 break;
2560 }
2561
2562 offset += i32::try_from(response_len).unwrap_or(BITMEX_MAX_TABLE_COUNT as i32);
2563 }
2564
2565 Ok(rates)
2566 }
2567
2568 pub async fn request_fill_reports(
2574 &self,
2575 instrument_id: Option<InstrumentId>,
2576 start: Option<DateTime<Utc>>,
2577 end: Option<DateTime<Utc>>,
2578 limit: Option<u32>,
2579 ) -> anyhow::Result<Vec<FillReport>> {
2580 if let (Some(start), Some(end)) = (start, end) {
2581 anyhow::ensure!(
2582 start < end,
2583 "Invalid time range: start={start:?} end={end:?}",
2584 );
2585 }
2586
2587 let mut params = GetExecutionParamsBuilder::default();
2588
2589 if let Some(instrument_id) = instrument_id {
2590 params.symbol(instrument_id.symbol.as_str());
2591 }
2592
2593 if let Some(start) = start {
2594 params.start_time(start);
2595 }
2596
2597 if let Some(end) = end {
2598 params.end_time(end);
2599 }
2600
2601 if let Some(limit) = limit {
2602 params.count(limit as i32);
2603 } else {
2604 params.count(500); }
2606 params.reverse(true); let params = params.build().map_err(|e| anyhow::anyhow!(e))?;
2609
2610 let response = self.inner.get_executions(params).await?;
2611
2612 let ts_init = self.generate_ts_init();
2613
2614 let mut reports = Vec::new();
2615
2616 for exec in response {
2617 if let Some(start) = start {
2618 match exec.transact_time {
2619 Some(timestamp) if timestamp < start => continue,
2620 Some(_) => {}
2621 None => {
2622 log::debug!("Skipping fill report without transact_time for bounded query");
2623 continue;
2624 }
2625 }
2626 }
2627
2628 if let Some(end) = end {
2629 match exec.transact_time {
2630 Some(timestamp) if timestamp > end => continue,
2631 Some(_) => {}
2632 None => {
2633 log::debug!("Skipping fill report without transact_time for bounded query");
2634 continue;
2635 }
2636 }
2637 }
2638
2639 let Some(symbol) = exec.symbol else {
2641 log::debug!("Skipping execution without symbol: {:?}", exec.exec_type);
2642 continue;
2643 };
2644 let symbol_str = symbol.to_string();
2645
2646 let instrument = match self.instrument_from_cache(symbol) {
2647 Ok(instrument) => instrument,
2648 Err(e) => {
2649 log::error!(
2650 "Instrument not found in cache for execution parsing: symbol={symbol_str}, {e}"
2651 );
2652 continue;
2653 }
2654 };
2655
2656 match parse_fill_report(&exec, &instrument, ts_init) {
2657 Ok(report) => reports.push(report),
2658 Err(e) => {
2659 let error_msg = e.to_string();
2661 if error_msg.starts_with("Skipping non-trade execution")
2662 || error_msg.starts_with("Skipping execution without order_id")
2663 {
2664 log::debug!("{e}");
2665 } else {
2666 log::error!("Failed to parse fill report: {e}");
2667 }
2668 }
2669 }
2670 }
2671
2672 Ok(reports)
2673 }
2674
2675 pub async fn request_position_status_reports(
2681 &self,
2682 ) -> anyhow::Result<Vec<PositionStatusReport>> {
2683 let params = GetPositionParamsBuilder::default()
2684 .count(500) .build()
2686 .map_err(|e| anyhow::anyhow!(e))?;
2687
2688 let response = self.inner.get_positions(params).await?;
2689
2690 let ts_init = self.generate_ts_init();
2691
2692 let mut reports = Vec::new();
2693
2694 for pos in response {
2695 let symbol = pos.symbol;
2696 let instrument = match self.instrument_from_cache(symbol) {
2697 Ok(instrument) => instrument,
2698 Err(e) => {
2699 log::error!(
2700 "Instrument not found in cache for position parsing: symbol={}, {e}",
2701 pos.symbol.as_str(),
2702 );
2703 continue;
2704 }
2705 };
2706
2707 match parse_position_report(&pos, &instrument, ts_init) {
2708 Ok(report) => reports.push(report),
2709 Err(e) => log::error!("Failed to parse position report: {e}"),
2710 }
2711 }
2712
2713 Ok(reports)
2714 }
2715
2716 pub async fn update_position_leverage(
2724 &self,
2725 symbol: &str,
2726 leverage: f64,
2727 ) -> anyhow::Result<PositionStatusReport> {
2728 let params = PostPositionLeverageParams {
2729 symbol: symbol.to_string(),
2730 leverage,
2731 target_account_id: None,
2732 };
2733
2734 let response = self.inner.update_position_leverage(params).await?;
2735
2736 let instrument = self.instrument_from_cache(Ustr::from(symbol))?;
2737 let ts_init = self.generate_ts_init();
2738
2739 parse_position_report(&response, &instrument, ts_init)
2740 }
2741}
2742
2743fn is_cancel_all_rejection(order: &BitmexOrder) -> bool {
2744 order.ord_status == Some(BitmexOrderStatus::Rejected)
2745 && order.ord_rej_reason.as_deref() == Some("Invalid orderID")
2746 && order.cl_ord_id.is_none()
2747 && order.order_qty.is_none()
2748 && order.leaves_qty.is_none()
2749 && order.cum_qty.is_none()
2750}
2751
2752fn parse_order_book_l2_snapshot(
2753 rows: &[BitmexOrderBookL2],
2754 instrument: &InstrumentAny,
2755 instrument_id: InstrumentId,
2756 ts_init: UnixNanos,
2757) -> anyhow::Result<OrderBookDeltas> {
2758 let price_precision = instrument.price_precision();
2759 let mut deltas = Vec::with_capacity(rows.len() + 1);
2760 deltas.push(OrderBookDelta::clear(instrument_id, 0, ts_init, ts_init));
2761
2762 for row in rows {
2763 if row.symbol != instrument_id.symbol.inner() {
2764 log::warn!(
2765 "Skipping BitMEX order book row for unexpected symbol: symbol={}, expected={}",
2766 row.symbol,
2767 instrument_id.symbol,
2768 );
2769 continue;
2770 }
2771
2772 let Some(price_value) = row.price else {
2773 log::warn!(
2774 "Skipping BitMEX order book row without price: symbol={}, id={}",
2775 row.symbol,
2776 row.id,
2777 );
2778 continue;
2779 };
2780
2781 let Some(size_value) = row.size else {
2782 log::warn!(
2783 "Skipping BitMEX order book row without size: symbol={}, id={}",
2784 row.symbol,
2785 row.id,
2786 );
2787 continue;
2788 };
2789
2790 let Ok(size) = u64::try_from(size_value) else {
2791 log::warn!(
2792 "Skipping BitMEX order book row with negative size: symbol={}, id={}, size={}",
2793 row.symbol,
2794 row.id,
2795 size_value,
2796 );
2797 continue;
2798 };
2799
2800 let Ok(order_id) = u64::try_from(row.id) else {
2801 log::warn!(
2802 "Skipping BitMEX order book row with negative id: symbol={}, id={}",
2803 row.symbol,
2804 row.id,
2805 );
2806 continue;
2807 };
2808
2809 let order = BookOrder::new(
2810 OrderSide::from(row.side),
2811 Price::new(price_value, price_precision),
2812 parse_contracts_quantity(size, instrument),
2813 order_id,
2814 );
2815 let delta = OrderBookDelta::new(
2816 instrument_id,
2817 BookAction::Add,
2818 order,
2819 RecordFlag::F_SNAPSHOT as u8,
2820 0,
2821 ts_init,
2822 ts_init,
2823 );
2824 deltas.push(delta);
2825 }
2826
2827 if let Some(last) = deltas.last_mut() {
2828 last.flags |= RecordFlag::F_LAST as u8;
2829 }
2830
2831 OrderBookDeltas::new_checked(instrument_id, deltas)
2832}
2833
2834fn parse_funding_rate_update(
2835 raw: &BitmexFunding,
2836 instrument_id: InstrumentId,
2837) -> Option<FundingRateUpdate> {
2838 let Some(rate) = raw.funding_rate else {
2839 log::warn!(
2840 "Skipping BitMEX funding rate without funding_rate: symbol={}, timestamp={}",
2841 raw.symbol,
2842 raw.timestamp,
2843 );
2844 return None;
2845 };
2846
2847 let interval = raw.funding_interval.map(|interval| {
2848 let minutes = interval.hour() * 60 + interval.minute();
2849 minutes as u16
2850 });
2851 let ts_event = UnixNanos::from(raw.timestamp);
2852
2853 Some(FundingRateUpdate::new(
2854 instrument_id,
2855 rate,
2856 interval,
2857 None,
2858 ts_event,
2859 ts_event,
2860 ))
2861}
2862
2863fn account_id_from_margins(margins: &[BitmexMargin]) -> anyhow::Result<Option<AccountId>> {
2864 let Some(first) = margins.first() else {
2865 return Ok(None);
2866 };
2867
2868 let account = first.account;
2869 if let Some(mismatch) = margins.iter().find(|margin| margin.account != account) {
2870 anyhow::bail!(
2871 "BitMEX returned inconsistent margin account IDs: {account} and {}",
2872 mismatch.account
2873 );
2874 }
2875
2876 Ok(Some(bitmex_account_id(account)))
2877}
2878
2879#[cfg(test)]
2880mod tests {
2881 use nautilus_core::UUID4;
2882 use nautilus_model::enums::OrderStatus;
2883 use rstest::rstest;
2884 use serde_json::json;
2885
2886 use super::*;
2887
2888 fn margin_with_account(account: i64) -> BitmexMargin {
2889 BitmexMargin {
2890 account,
2891 currency: Ustr::from("XBt"),
2892 risk_limit: None,
2893 prev_state: None,
2894 state: None,
2895 action: None,
2896 amount: None,
2897 pending_credit: None,
2898 pending_debit: None,
2899 confirmed_debit: None,
2900 prev_realised_pnl: None,
2901 prev_unrealised_pnl: None,
2902 gross_comm: None,
2903 gross_open_cost: None,
2904 gross_open_premium: None,
2905 gross_exec_cost: None,
2906 gross_mark_value: None,
2907 risk_value: None,
2908 taxable_margin: None,
2909 init_margin: None,
2910 maint_margin: None,
2911 session_margin: None,
2912 target_excess_margin: None,
2913 var_margin: None,
2914 realised_pnl: None,
2915 unrealised_pnl: None,
2916 indicative_tax: None,
2917 unrealised_profit: None,
2918 synthetic_margin: None,
2919 wallet_balance: None,
2920 margin_balance: None,
2921 margin_balance_pcnt: None,
2922 margin_leverage: None,
2923 margin_used_pcnt: None,
2924 excess_margin: None,
2925 excess_margin_pcnt: None,
2926 available_margin: None,
2927 withdrawable_margin: None,
2928 timestamp: None,
2929 gross_last_value: None,
2930 commission: None,
2931 }
2932 }
2933
2934 fn build_report(
2935 client_order_id: &str,
2936 venue_order_id: &str,
2937 contingency_type: ContingencyType,
2938 order_list_id: Option<&str>,
2939 ) -> OrderStatusReport {
2940 let mut report = OrderStatusReport::new(
2941 AccountId::from("BITMEX-1"),
2942 InstrumentId::from("XBTUSD.BITMEX"),
2943 Some(ClientOrderId::from(client_order_id)),
2944 VenueOrderId::from(venue_order_id),
2945 OrderSide::Buy,
2946 OrderType::Limit,
2947 TimeInForce::Gtc,
2948 OrderStatus::Accepted,
2949 Quantity::new(100.0, 0),
2950 Quantity::default(),
2951 UnixNanos::from(1_u64),
2952 UnixNanos::from(1_u64),
2953 UnixNanos::from(1_u64),
2954 Some(UUID4::new()),
2955 );
2956
2957 if let Some(id) = order_list_id {
2958 report = report.with_order_list_id(OrderListId::from(id));
2959 }
2960
2961 report.with_contingency_type(contingency_type)
2962 }
2963
2964 #[rstest]
2965 fn test_account_id_from_margins_uses_bitmex_account_number() {
2966 let margins = vec![margin_with_account(319111), margin_with_account(319111)];
2967
2968 let account_id = account_id_from_margins(&margins).unwrap().unwrap();
2969
2970 assert_eq!(account_id, AccountId::from("BITMEX-319111"));
2971 }
2972
2973 #[rstest]
2974 fn test_account_id_from_margins_empty_returns_none() {
2975 let margins = [];
2976
2977 let account_id = account_id_from_margins(&margins).unwrap();
2978
2979 assert_eq!(account_id, None);
2980 }
2981
2982 #[rstest]
2983 fn test_account_id_from_margins_rejects_inconsistent_accounts() {
2984 let margins = vec![margin_with_account(319111), margin_with_account(319112)];
2985
2986 let err = account_id_from_margins(&margins).unwrap_err();
2987
2988 assert!(err.to_string().contains("inconsistent margin account IDs"));
2989 }
2990
2991 #[rstest]
2992 fn test_cancel_all_rejection_requires_unavailable_order_shape() {
2993 let unavailable: BitmexOrder = serde_json::from_str(include_str!(
2994 "../../test_data/http_cancel_all_close_race.json"
2995 ))
2996 .unwrap();
2997 let mut with_client_id = unavailable.clone();
2998 with_client_id.cl_ord_id = Some(Ustr::from("tracked-rejection"));
2999 let mut with_order_qty = unavailable.clone();
3000 with_order_qty.order_qty = Some(100);
3001 let mut with_leaves_qty = unavailable.clone();
3002 with_leaves_qty.leaves_qty = Some(0);
3003 let mut with_cum_qty = unavailable.clone();
3004 with_cum_qty.cum_qty = Some(0);
3005 let mut with_other_reason = unavailable.clone();
3006 with_other_reason.ord_rej_reason = Some(Ustr::from("Insufficient margin"));
3007
3008 let unavailable_result = is_cancel_all_rejection(&unavailable);
3009 let preserved = [
3010 ("client order ID", with_client_id),
3011 ("order quantity", with_order_qty),
3012 ("leaves quantity", with_leaves_qty),
3013 ("cumulative quantity", with_cum_qty),
3014 ("different rejection reason", with_other_reason),
3015 ];
3016
3017 assert!(unavailable_result);
3018 for (case, order) in preserved {
3019 assert!(!is_cancel_all_rejection(&order), "preserved {case}");
3020 }
3021 }
3022
3023 #[rstest]
3024 fn test_sign_request_generates_correct_headers() {
3025 let client = BitmexRawHttpClient::with_credentials(
3026 "test_api_key".to_string(),
3027 "test_api_secret".to_string(),
3028 "http://localhost:8080".to_string(),
3029 60,
3030 3,
3031 1_000,
3032 10_000,
3033 10_000,
3034 10,
3035 120,
3036 None,
3037 )
3038 .expect("Failed to create test client");
3039
3040 let headers = client
3041 .sign_request(&Method::GET, "/api/v1/order", None)
3042 .unwrap();
3043
3044 assert!(headers.contains_key("api-key"));
3045 assert!(headers.contains_key("api-signature"));
3046 assert!(headers.contains_key("api-expires"));
3047 assert_eq!(headers.get("api-key").unwrap(), "test_api_key");
3048 }
3049
3050 #[rstest]
3051 fn test_sign_request_with_body() {
3052 let client = BitmexRawHttpClient::with_credentials(
3053 "test_api_key".to_string(),
3054 "test_api_secret".to_string(),
3055 "http://localhost:8080".to_string(),
3056 60,
3057 3,
3058 1_000,
3059 10_000,
3060 10_000,
3061 10,
3062 120,
3063 None,
3064 )
3065 .expect("Failed to create test client");
3066
3067 let body = json!({"symbol": "XBTUSD", "orderQty": 100});
3068 let body_bytes = serde_json::to_vec(&body).unwrap();
3069
3070 let headers_without_body = client
3071 .sign_request(&Method::POST, "/api/v1/order", None)
3072 .unwrap();
3073 let headers_with_body = client
3074 .sign_request(&Method::POST, "/api/v1/order", Some(&body_bytes))
3075 .unwrap();
3076
3077 assert_ne!(
3079 headers_without_body.get("api-signature").unwrap(),
3080 headers_with_body.get("api-signature").unwrap()
3081 );
3082 }
3083
3084 #[rstest]
3085 fn test_sign_request_uses_custom_recv_window() {
3086 let client_default = BitmexRawHttpClient::with_credentials(
3087 "test_api_key".to_string(),
3088 "test_api_secret".to_string(),
3089 "http://localhost:8080".to_string(),
3090 60,
3091 3,
3092 1_000,
3093 10_000,
3094 10_000, 10,
3096 120,
3097 None,
3098 )
3099 .expect("Failed to create test client");
3100
3101 let client_custom = BitmexRawHttpClient::with_credentials(
3102 "test_api_key".to_string(),
3103 "test_api_secret".to_string(),
3104 "http://localhost:8080".to_string(),
3105 60,
3106 3,
3107 1_000,
3108 10_000,
3109 30_000, 10,
3111 120,
3112 None,
3113 )
3114 .expect("Failed to create test client");
3115
3116 let headers_default = client_default
3117 .sign_request(&Method::GET, "/api/v1/order", None)
3118 .unwrap();
3119 let headers_custom = client_custom
3120 .sign_request(&Method::GET, "/api/v1/order", None)
3121 .unwrap();
3122
3123 let expires_default: i64 = headers_default.get("api-expires").unwrap().parse().unwrap();
3125 let expires_custom: i64 = headers_custom.get("api-expires").unwrap().parse().unwrap();
3126
3127 let now = Utc::now().timestamp();
3129 assert!(expires_default > now);
3130 assert!(expires_custom > now);
3131
3132 assert!(expires_custom > expires_default);
3134
3135 let diff = expires_custom - expires_default;
3138 assert!((18..=25).contains(&diff));
3139 }
3140
3141 #[rstest]
3142 fn test_populate_linked_order_ids_from_order_list() {
3143 let base = "O-20250922-002219-001-000";
3144 let entry = format!("{base}-1");
3145 let stop = format!("{base}-2");
3146 let take = format!("{base}-3");
3147
3148 let mut reports = vec![
3149 build_report(&entry, "V-1", ContingencyType::Oto, Some("OL-1")),
3150 build_report(&stop, "V-2", ContingencyType::Ouo, Some("OL-1")),
3151 build_report(&take, "V-3", ContingencyType::Ouo, Some("OL-1")),
3152 ];
3153
3154 BitmexHttpClient::populate_linked_order_ids(&mut reports);
3155
3156 assert_eq!(
3157 reports[0].linked_order_ids,
3158 Some(vec![
3159 ClientOrderId::from(stop.as_str()),
3160 ClientOrderId::from(take.as_str()),
3161 ]),
3162 );
3163 assert_eq!(
3164 reports[1].linked_order_ids,
3165 Some(vec![
3166 ClientOrderId::from(entry.as_str()),
3167 ClientOrderId::from(take.as_str()),
3168 ]),
3169 );
3170 assert_eq!(
3171 reports[2].linked_order_ids,
3172 Some(vec![
3173 ClientOrderId::from(entry.as_str()),
3174 ClientOrderId::from(stop.as_str()),
3175 ]),
3176 );
3177 }
3178
3179 #[rstest]
3180 fn test_populate_linked_order_ids_from_id_prefix() {
3181 let base = "O-20250922-002220-001-000";
3182 let entry = format!("{base}-1");
3183 let stop = format!("{base}-2");
3184 let take = format!("{base}-3");
3185
3186 let mut reports = vec![
3187 build_report(&entry, "V-1", ContingencyType::Oto, None),
3188 build_report(&stop, "V-2", ContingencyType::Ouo, None),
3189 build_report(&take, "V-3", ContingencyType::Ouo, None),
3190 ];
3191
3192 BitmexHttpClient::populate_linked_order_ids(&mut reports);
3193
3194 assert_eq!(
3195 reports[0].linked_order_ids,
3196 Some(vec![
3197 ClientOrderId::from(stop.as_str()),
3198 ClientOrderId::from(take.as_str()),
3199 ]),
3200 );
3201 assert_eq!(
3202 reports[1].linked_order_ids,
3203 Some(vec![
3204 ClientOrderId::from(entry.as_str()),
3205 ClientOrderId::from(take.as_str()),
3206 ]),
3207 );
3208 assert_eq!(
3209 reports[2].linked_order_ids,
3210 Some(vec![
3211 ClientOrderId::from(entry.as_str()),
3212 ClientOrderId::from(stop.as_str()),
3213 ]),
3214 );
3215 }
3216
3217 #[rstest]
3218 fn test_populate_linked_order_ids_respects_non_contingent_orders() {
3219 let base = "O-20250922-002221-001-000";
3220 let entry = format!("{base}-1");
3221 let passive = format!("{base}-2");
3222
3223 let mut reports = vec![
3224 build_report(&entry, "V-1", ContingencyType::NoContingency, None),
3225 build_report(&passive, "V-2", ContingencyType::Ouo, None),
3226 ];
3227
3228 BitmexHttpClient::populate_linked_order_ids(&mut reports);
3229
3230 assert!(reports[0].linked_order_ids.is_none());
3232
3233 assert!(reports[1].linked_order_ids.is_none());
3235 assert_eq!(reports[1].contingency_type, ContingencyType::NoContingency);
3236 }
3237
3238 #[rstest]
3239 fn test_populate_linked_order_ids_treats_orphaned_oto_as_standalone() {
3240 let mut reports = vec![build_report(
3241 "O-20250922-002222-001-000-1",
3242 "V-1",
3243 ContingencyType::Oto,
3244 Some("OL-1"),
3245 )];
3246
3247 BitmexHttpClient::populate_linked_order_ids(&mut reports);
3248
3249 assert!(reports[0].linked_order_ids.is_none());
3250 assert_eq!(reports[0].contingency_type, ContingencyType::NoContingency);
3251 assert_eq!(reports[0].parent_order_id, None);
3252 }
3253}