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