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