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