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