1use std::{num::NonZero, str::FromStr};
19
20use ahash::AHashMap;
21use jiff::tz::Offset;
22use nautilus_core::{UnixNanos, uuid::UUID4};
23#[cfg(test)]
24use nautilus_model::types::Currency;
25use nautilus_model::{
26 data::{
27 Bar, BarSpecification, BarType, BookOrder, Data, FundingRateUpdate, IndexPriceUpdate,
28 MarkPriceUpdate, OrderBookDelta, OrderBookDepth, QuoteTick, TradeTick, depth::DEPTH10_LEN,
29 },
30 enums::{
31 AccountType, AggregationSource, BarAggregation, OrderSide, OrderStatus, OrderType,
32 PriceType, RecordFlag, TimeInForce, TrailingOffsetType,
33 },
34 events::{
35 OrderAccepted, OrderCanceled, OrderExpired, OrderRejected, OrderTriggered, OrderUpdated,
36 account::state::AccountState,
37 },
38 identifiers::{
39 AccountId, ClientOrderId, InstrumentId, OrderListId, StrategyId, Symbol, TradeId, TraderId,
40 VenueOrderId,
41 },
42 instruments::{Instrument, InstrumentAny},
43 reports::{FillReport, OrderStatusReport, PositionStatusReport},
44 types::{AccountBalance, MarginBalance, Money, Price, Quantity},
45};
46use rust_decimal::Decimal;
47use ustr::Ustr;
48
49use super::{
50 enums::{BitmexAction, BitmexWsTopic},
51 messages::{
52 BitmexExecutionMsg, BitmexFundingMsg, BitmexInstrumentMsg, BitmexMarginMsg,
53 BitmexOrderBook10Msg, BitmexOrderBookMsg, BitmexOrderMsg, BitmexPositionMsg,
54 BitmexQuoteMsg, BitmexTradeBinMsg, BitmexTradeMsg, BitmexWalletMsg,
55 },
56};
57use crate::{
58 common::{
59 consts::BITMEX_VENUE,
60 enums::{
61 BitmexExecInstruction, BitmexExecType, BitmexOrderStatus, BitmexOrderType,
62 BitmexPegPriceType, BitmexSide,
63 },
64 parse::{
65 bitmex_account_id, bitmex_currency_divisor, clean_reason, derive_trade_id,
66 extract_trigger_type, map_bitmex_currency, normalize_trade_bin_prices,
67 normalize_trade_bin_volume, parse_account_balance, parse_contracts_quantity,
68 parse_fractional_quantity, parse_instrument_id, parse_liquidity_side,
69 parse_optional_datetime_to_unix_nanos, parse_position_side,
70 parse_signed_contracts_quantity,
71 },
72 },
73 http::parse::get_currency,
74 websocket::messages::BitmexOrderUpdateMsg,
75};
76
77const BAR_SPEC_1_MINUTE: BarSpecification = BarSpecification {
78 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
79 aggregation: BarAggregation::Minute,
80 price_type: PriceType::Last,
81};
82const BAR_SPEC_5_MINUTE: BarSpecification = BarSpecification {
83 step: NonZero::new(5).expect("5 is a valid non-zero usize"),
84 aggregation: BarAggregation::Minute,
85 price_type: PriceType::Last,
86};
87const BAR_SPEC_1_HOUR: BarSpecification = BarSpecification {
88 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
89 aggregation: BarAggregation::Hour,
90 price_type: PriceType::Last,
91};
92const BAR_SPEC_1_DAY: BarSpecification = BarSpecification {
93 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
94 aggregation: BarAggregation::Day,
95 price_type: PriceType::Last,
96};
97
98#[inline]
106#[must_use]
107pub fn is_index_symbol(symbol: &Ustr) -> bool {
108 symbol.starts_with('.')
109}
110
111#[must_use]
113pub fn parse_book_msg_vec(
114 data: Vec<BitmexOrderBookMsg>,
115 action: BitmexAction,
116 instruments: &AHashMap<Ustr, InstrumentAny>,
117 ts_init: UnixNanos,
118) -> Vec<Data> {
119 let mut deltas = Vec::with_capacity(data.len());
120
121 for msg in data {
122 if let Some(instrument) = instruments.get(&msg.symbol) {
123 let instrument_id = instrument.id();
124 let price_precision = instrument.price_precision();
125 deltas.push(Data::BookDelta(parse_book_msg(
126 &msg,
127 &action,
128 instrument,
129 instrument_id,
130 price_precision,
131 ts_init,
132 )));
133 } else {
134 log::error!(
135 "Instrument cache miss: book delta dropped for symbol={}",
136 msg.symbol
137 );
138 }
139 }
140
141 if let Some(Data::BookDelta(last_delta)) = deltas.last_mut() {
143 *last_delta = OrderBookDelta::new(
144 last_delta.instrument_id,
145 last_delta.action,
146 last_delta.order,
147 last_delta.flags | RecordFlag::F_LAST as u8,
148 last_delta.sequence,
149 last_delta.ts_event,
150 last_delta.ts_init,
151 );
152 }
153
154 deltas
155}
156
157#[must_use]
159pub fn parse_book10_msg_vec(
160 data: Vec<BitmexOrderBook10Msg>,
161 instruments: &AHashMap<Ustr, InstrumentAny>,
162 ts_init: UnixNanos,
163) -> Vec<Data> {
164 let mut depths = Vec::with_capacity(data.len());
165
166 for msg in data {
167 if let Some(instrument) = instruments.get(&msg.symbol) {
168 let instrument_id = instrument.id();
169 let price_precision = instrument.price_precision();
170 match parse_book10_msg(&msg, instrument, instrument_id, price_precision, ts_init) {
171 Ok(depth) => depths.push(Data::BookDepth(Box::new(depth))),
172 Err(e) => {
173 log::error!("Failed to parse orderBook10 for symbol={}: {e}", msg.symbol);
174 }
175 }
176 } else {
177 log::error!(
178 "Instrument cache miss: depth message dropped for symbol={}",
179 msg.symbol
180 );
181 }
182 }
183 depths
184}
185
186#[must_use]
188pub fn parse_trade_msg_vec(
189 data: Vec<BitmexTradeMsg>,
190 instruments: &AHashMap<Ustr, InstrumentAny>,
191 ts_init: UnixNanos,
192) -> Vec<Data> {
193 let mut trades = Vec::with_capacity(data.len());
194
195 for msg in data {
196 if let Some(instrument) = instruments.get(&msg.symbol) {
197 let instrument_id = instrument.id();
198 let price_precision = instrument.price_precision();
199 trades.push(Data::Trade(parse_trade_msg(
200 &msg,
201 instrument,
202 instrument_id,
203 price_precision,
204 ts_init,
205 )));
206 } else {
207 log::error!(
208 "Instrument cache miss: trade message dropped for symbol={}",
209 msg.symbol
210 );
211 }
212 }
213 trades
214}
215
216#[must_use]
218pub fn parse_trade_bin_msg_vec(
219 data: Vec<BitmexTradeBinMsg>,
220 topic: &BitmexWsTopic,
221 instruments: &AHashMap<Ustr, InstrumentAny>,
222 ts_init: UnixNanos,
223) -> Vec<Data> {
224 let mut trades = Vec::with_capacity(data.len());
225
226 for msg in data {
227 if let Some(instrument) = instruments.get(&msg.symbol) {
228 let instrument_id = instrument.id();
229 let price_precision = instrument.price_precision();
230 trades.push(Data::Bar(parse_trade_bin_msg(
231 &msg,
232 topic,
233 instrument,
234 instrument_id,
235 price_precision,
236 ts_init,
237 )));
238 } else {
239 log::error!(
240 "Instrument cache miss: trade bin (bar) dropped for symbol={}",
241 msg.symbol
242 );
243 }
244 }
245 trades
246}
247
248#[must_use]
250pub fn parse_book_msg(
251 msg: &BitmexOrderBookMsg,
252 action: &BitmexAction,
253 instrument: &InstrumentAny,
254 instrument_id: InstrumentId,
255 price_precision: u8,
256 ts_init: UnixNanos,
257) -> OrderBookDelta {
258 let flags = if action == &BitmexAction::Partial {
259 RecordFlag::F_SNAPSHOT as u8
260 } else {
261 0
262 };
263
264 let action = action.as_book_action();
265 let price = Price::new(msg.price, price_precision);
266 let side = msg.side.as_order_side();
267 let size = parse_contracts_quantity(msg.size.unwrap_or(0), instrument);
268 let order_id = msg.id;
269 let order = BookOrder::new(side, price, size, order_id);
270 let sequence = 0; let ts_event = UnixNanos::from(msg.timestamp);
272
273 OrderBookDelta::new(
274 instrument_id,
275 action,
276 order,
277 flags,
278 sequence,
279 ts_event,
280 ts_init,
281 )
282}
283
284pub fn parse_book10_msg(
290 msg: &BitmexOrderBook10Msg,
291 instrument: &InstrumentAny,
292 instrument_id: InstrumentId,
293 price_precision: u8,
294 ts_init: UnixNanos,
295) -> anyhow::Result<OrderBookDepth> {
296 let mut bids = Vec::with_capacity(DEPTH10_LEN);
297 let mut asks = Vec::with_capacity(DEPTH10_LEN);
298
299 let mut bid_counts: [u32; DEPTH10_LEN] = [0; DEPTH10_LEN];
301 let mut ask_counts: [u32; DEPTH10_LEN] = [0; DEPTH10_LEN];
302
303 for (i, level) in msg.bids.iter().enumerate() {
304 let bid_order = BookOrder::new(
305 OrderSide::Buy,
306 Price::new(level[0], price_precision),
307 parse_fractional_quantity(level[1], instrument),
308 0,
309 );
310
311 bids.push(bid_order);
312 bid_counts[i] = 1;
313 }
314
315 for (i, level) in msg.asks.iter().enumerate() {
316 let ask_order = BookOrder::new(
317 OrderSide::Sell,
318 Price::new(level[0], price_precision),
319 parse_fractional_quantity(level[1], instrument),
320 0,
321 );
322
323 asks.push(ask_order);
324 ask_counts[i] = 1;
325 }
326
327 let bids: [BookOrder; DEPTH10_LEN] = bids.try_into().map_err(|v: Vec<BookOrder>| {
328 anyhow::anyhow!(
329 "Bids length mismatch: expected {DEPTH10_LEN}, was {}",
330 v.len()
331 )
332 })?;
333 let asks: [BookOrder; DEPTH10_LEN] = asks.try_into().map_err(|v: Vec<BookOrder>| {
334 anyhow::anyhow!(
335 "Asks length mismatch: expected {DEPTH10_LEN}, was {}",
336 v.len()
337 )
338 })?;
339
340 let ts_event = UnixNanos::from(msg.timestamp);
341
342 Ok(OrderBookDepth::new(
343 instrument_id,
344 bids,
345 asks,
346 bid_counts,
347 ask_counts,
348 RecordFlag::F_SNAPSHOT as u8,
349 0, ts_event,
351 ts_init,
352 ))
353}
354
355#[must_use]
357pub fn parse_quote_msg(
358 msg: &BitmexQuoteMsg,
359 last_quote: &QuoteTick,
360 instrument: &InstrumentAny,
361 instrument_id: InstrumentId,
362 price_precision: u8,
363 ts_init: UnixNanos,
364) -> QuoteTick {
365 let bid_price = match msg.bid_price {
366 Some(price) => Price::new(price, price_precision),
367 None => last_quote.bid_price,
368 };
369
370 let ask_price = match msg.ask_price {
371 Some(price) => Price::new(price, price_precision),
372 None => last_quote.ask_price,
373 };
374
375 let bid_size = match msg.bid_size {
376 Some(size) => parse_contracts_quantity(size, instrument),
377 None => last_quote.bid_size,
378 };
379
380 let ask_size = match msg.ask_size {
381 Some(size) => parse_contracts_quantity(size, instrument),
382 None => last_quote.ask_size,
383 };
384
385 let ts_event = UnixNanos::from(msg.timestamp);
386
387 QuoteTick::new(
388 instrument_id,
389 bid_price,
390 ask_price,
391 bid_size,
392 ask_size,
393 ts_event,
394 ts_init,
395 )
396}
397
398#[must_use]
400pub fn parse_trade_msg(
401 msg: &BitmexTradeMsg,
402 instrument: &InstrumentAny,
403 instrument_id: InstrumentId,
404 price_precision: u8,
405 ts_init: UnixNanos,
406) -> TradeTick {
407 let price = Price::new(msg.price, price_precision);
408 let size = parse_contracts_quantity(msg.size, instrument);
409 let aggressor_side = msg.side.as_aggressor_side();
410 let ts_event = UnixNanos::from(msg.timestamp);
411 let trade_id = match msg.trd_match_id {
412 Some(uuid) => TradeId::new(uuid.to_string()),
413 None => derive_trade_id(
414 msg.symbol,
415 ts_event.as_u64(),
416 msg.price,
417 msg.size as i64,
418 Some(msg.side.into()),
419 ),
420 };
421
422 TradeTick::new(
423 instrument_id,
424 price,
425 size,
426 aggressor_side,
427 trade_id,
428 ts_event,
429 ts_init,
430 )
431}
432
433#[must_use]
435pub fn parse_trade_bin_msg(
436 msg: &BitmexTradeBinMsg,
437 topic: &BitmexWsTopic,
438 instrument: &InstrumentAny,
439 instrument_id: InstrumentId,
440 price_precision: u8,
441 ts_init: UnixNanos,
442) -> Bar {
443 let spec = bar_spec_from_topic(topic);
444 let bar_type = BarType::new(instrument_id, spec, AggregationSource::External);
445
446 let open = Price::new(msg.open, price_precision);
447 let high = Price::new(msg.high, price_precision);
448 let low = Price::new(msg.low, price_precision);
449 let close = Price::new(msg.close, price_precision);
450
451 let (open, high, low, close) =
452 normalize_trade_bin_prices(open, high, low, close, &msg.symbol, Some(&bar_type));
453
454 let volume_contracts = normalize_trade_bin_volume(Some(msg.volume), &msg.symbol);
455 let volume = parse_contracts_quantity(volume_contracts, instrument);
456 let ts_event = UnixNanos::from(msg.timestamp);
457
458 Bar::new(bar_type, open, high, low, close, volume, ts_event, ts_init)
459}
460
461#[must_use]
465pub fn bar_spec_from_topic(topic: &BitmexWsTopic) -> BarSpecification {
466 match topic {
467 BitmexWsTopic::TradeBin1m => BAR_SPEC_1_MINUTE,
468 BitmexWsTopic::TradeBin5m => BAR_SPEC_5_MINUTE,
469 BitmexWsTopic::TradeBin1h => BAR_SPEC_1_HOUR,
470 BitmexWsTopic::TradeBin1d => BAR_SPEC_1_DAY,
471 _ => {
472 log::error!("Bar specification not supported: topic={topic:?}");
473 BAR_SPEC_1_MINUTE
474 }
475 }
476}
477
478#[must_use]
482pub fn topic_from_bar_spec(spec: BarSpecification) -> BitmexWsTopic {
483 match spec {
484 BAR_SPEC_1_MINUTE => BitmexWsTopic::TradeBin1m,
485 BAR_SPEC_5_MINUTE => BitmexWsTopic::TradeBin5m,
486 BAR_SPEC_1_HOUR => BitmexWsTopic::TradeBin1h,
487 BAR_SPEC_1_DAY => BitmexWsTopic::TradeBin1d,
488 _ => {
489 log::error!("Bar specification not supported: spec={spec:?}");
490 BitmexWsTopic::TradeBin1m
491 }
492 }
493}
494
495fn infer_order_type_from_msg(msg: &BitmexOrderMsg) -> OrderType {
496 if msg.stop_px.is_some() {
497 if msg.price.is_some() {
498 OrderType::StopLimit
499 } else {
500 OrderType::StopMarket
501 }
502 } else if msg.price.is_some() {
503 OrderType::Limit
504 } else {
505 OrderType::Market
506 }
507}
508
509pub fn parse_order_msg(
519 msg: &BitmexOrderMsg,
520 instrument: &InstrumentAny,
521 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
522 ts_init: UnixNanos,
523) -> anyhow::Result<OrderStatusReport> {
524 let account_id = bitmex_account_id(msg.account);
525 let instrument_id = parse_instrument_id(msg.symbol);
526 let venue_order_id = VenueOrderId::new(msg.order_id.to_string());
527 let common_side: BitmexSide = msg.side.into();
528 let order_side = OrderSide::from(common_side);
529
530 let order_type: OrderType = if let Some(ord_type) = msg.ord_type {
531 if ord_type == BitmexOrderType::Pegged
533 && msg.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
534 {
535 if msg.price.is_some() {
536 OrderType::TrailingStopLimit
537 } else {
538 OrderType::TrailingStopMarket
539 }
540 } else {
541 ord_type.into()
542 }
543 } else if let Some(client_order_id) = msg.cl_ord_id {
544 let client_order_id = ClientOrderId::new(client_order_id);
545 if let Some(&cached) = order_type_cache.get(&client_order_id) {
546 cached
547 } else {
548 let inferred = infer_order_type_from_msg(msg);
549 order_type_cache.insert(client_order_id, inferred);
550 inferred
551 }
552 } else {
553 infer_order_type_from_msg(msg)
554 };
555
556 let time_in_force: TimeInForce = match msg.time_in_force {
557 Some(tif) => tif.try_into().map_err(|e| anyhow::anyhow!("{e}"))?,
558 None => TimeInForce::Gtc,
559 };
560 let order_status: OrderStatus = msg.ord_status.into();
561 let quantity = parse_signed_contracts_quantity(msg.order_qty, instrument);
562 let filled_qty = parse_signed_contracts_quantity(msg.cum_qty, instrument);
563 let report_id = UUID4::new();
564 let ts_accepted =
565 parse_optional_datetime_to_unix_nanos(&Some(msg.transact_time), "transact_time");
566 let ts_last = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "timestamp");
567
568 let mut report = OrderStatusReport::new(
569 account_id,
570 instrument_id,
571 None, venue_order_id,
573 order_side.into(),
574 order_type,
575 time_in_force,
576 order_status,
577 quantity,
578 filled_qty,
579 ts_accepted,
580 ts_last,
581 ts_init,
582 Some(report_id),
583 );
584
585 if let Some(cl_ord_id) = &msg.cl_ord_id {
586 report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
587 }
588
589 if let Some(cl_ord_link_id) = &msg.cl_ord_link_id {
590 report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
591 }
592
593 if let Some(price) = msg.price {
594 report = report.with_price(Price::new(price, instrument.price_precision()));
595 }
596
597 if let Some(avg_px) = msg.avg_px {
598 report = report.with_avg_px(avg_px);
599 }
600
601 if let Some(trigger_price) = msg.stop_px {
602 report = report
603 .with_trigger_price(Price::new(trigger_price, instrument.price_precision()))
604 .with_trigger_type(extract_trigger_type(msg.exec_inst.as_ref()));
605 }
606
607 if matches!(
609 order_type,
610 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
611 ) && let Some(peg_offset) = msg.peg_offset_value
612 {
613 let trailing_offset = Decimal::try_from(peg_offset.abs())
614 .unwrap_or_else(|_| Decimal::new(peg_offset.abs() as i64, 0));
615 report = report
616 .with_trailing_offset(trailing_offset)
617 .with_trailing_offset_type(TrailingOffsetType::Price);
618
619 if msg.stop_px.is_none() {
620 report = report.with_trigger_type(extract_trigger_type(msg.exec_inst.as_ref()));
621 }
622 }
623
624 if let Some(exec_insts) = &msg.exec_inst {
625 for exec_inst in exec_insts {
626 match exec_inst {
627 BitmexExecInstruction::ParticipateDoNotInitiate => {
628 report = report.with_post_only(true);
629 }
630 BitmexExecInstruction::ReduceOnly => {
631 report = report.with_reduce_only(true);
632 }
633 _ => {}
634 }
635 }
636 }
637
638 if order_status == OrderStatus::Rejected {
640 if let Some(reason_str) = msg.ord_rej_reason.or(msg.text) {
641 log::debug!(
642 "Order rejected with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
643 venue_order_id,
644 msg.cl_ord_id,
645 reason_str,
646 );
647 report = report.with_cancel_reason(clean_reason(reason_str.as_ref()));
648 } else {
649 log::debug!(
650 "Order rejected without reason from BitMEX: order_id={:?}, client_order_id={:?}, ord_status={:?}, ord_rej_reason={:?}, text={:?}",
651 venue_order_id,
652 msg.cl_ord_id,
653 msg.ord_status,
654 msg.ord_rej_reason,
655 msg.text,
656 );
657 }
658 }
659
660 if order_status == OrderStatus::Canceled
663 && let Some(reason_str) = msg.ord_rej_reason.or(msg.text)
664 {
665 report = report.with_cancel_reason(clean_reason(reason_str.as_ref()));
666 }
667
668 Ok(report)
669}
670
671#[derive(Debug, Clone)]
673pub enum ParsedOrderEvent {
674 Accepted(OrderAccepted),
675 Canceled(OrderCanceled),
676 Expired(OrderExpired),
677 Triggered(OrderTriggered),
678 Rejected(OrderRejected),
679}
680
681pub fn parse_order_event(
687 msg: &BitmexOrderMsg,
688 client_order_id: ClientOrderId,
689 account_id: AccountId,
690 trader_id: TraderId,
691 strategy_id: StrategyId,
692 ts_init: UnixNanos,
693) -> Option<ParsedOrderEvent> {
694 let instrument_id = parse_instrument_id(msg.symbol);
695 let venue_order_id = VenueOrderId::new(msg.order_id.to_string());
696 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "timestamp");
697
698 match msg.ord_status {
699 BitmexOrderStatus::New => {
700 let accepted = OrderAccepted::new(
701 trader_id,
702 strategy_id,
703 instrument_id,
704 client_order_id,
705 venue_order_id,
706 account_id,
707 UUID4::new(),
708 ts_event,
709 ts_init,
710 false,
711 );
712 Some(ParsedOrderEvent::Accepted(accepted))
713 }
714 BitmexOrderStatus::Canceled => {
715 let cancel_reason = msg
718 .ord_rej_reason
719 .or(msg.text)
720 .map(|r| clean_reason(r.as_ref()));
721
722 let is_post_only_rejection = cancel_reason
723 .as_deref()
724 .is_some_and(|r| r.contains("ParticipateDoNotInitiate"));
725
726 if is_post_only_rejection {
727 let rejected = OrderRejected::new(
728 trader_id,
729 strategy_id,
730 instrument_id,
731 client_order_id,
732 account_id,
733 Ustr::from(
734 cancel_reason
735 .as_deref()
736 .unwrap_or("Post-only order rejected"),
737 ),
738 UUID4::new(),
739 ts_event,
740 ts_init,
741 false,
742 true, );
744 Some(ParsedOrderEvent::Rejected(rejected))
745 } else {
746 let canceled = OrderCanceled::new(
747 trader_id,
748 strategy_id,
749 instrument_id,
750 client_order_id,
751 UUID4::new(),
752 ts_event,
753 ts_init,
754 false,
755 Some(venue_order_id),
756 Some(account_id),
757 cancel_reason.as_deref().map(Ustr::from),
758 );
759 Some(ParsedOrderEvent::Canceled(canceled))
760 }
761 }
762 BitmexOrderStatus::Expired => {
763 let expired = OrderExpired::new(
764 trader_id,
765 strategy_id,
766 instrument_id,
767 client_order_id,
768 UUID4::new(),
769 ts_event,
770 ts_init,
771 false,
772 Some(venue_order_id),
773 Some(account_id),
774 );
775 Some(ParsedOrderEvent::Expired(expired))
776 }
777 _ => None,
781 }
782}
783
784pub fn parse_order_update_msg(
788 msg: &BitmexOrderUpdateMsg,
789 instrument: &InstrumentAny,
790 account_id: AccountId,
791 ts_init: UnixNanos,
792) -> Option<OrderUpdated> {
793 let trader_id = TraderId::external();
795 let strategy_id = StrategyId::external();
796 let instrument_id = parse_instrument_id(msg.symbol?);
797 let venue_order_id = Some(VenueOrderId::new(msg.order_id.to_string()));
798 let client_order_id = msg
799 .cl_ord_id
800 .as_ref()
801 .map_or_else(ClientOrderId::external, ClientOrderId::new);
802
803 let quantity = match (msg.leaves_qty, msg.cum_qty) {
806 (Some(leaves), Some(cum)) => parse_contracts_quantity((leaves + cum) as u64, instrument),
807 _ => Quantity::zero(instrument.size_precision()),
808 };
809 let price = msg
810 .price
811 .value()
812 .copied()
813 .map(|p| Price::new(p, instrument.price_precision()));
814
815 let trigger_price = None;
817 let protection_price = None;
819
820 let event_id = UUID4::new();
821 let ts_event = parse_optional_datetime_to_unix_nanos(&msg.timestamp, "timestamp");
822
823 Some(OrderUpdated::new(
824 trader_id,
825 strategy_id,
826 instrument_id,
827 client_order_id,
828 quantity,
829 event_id,
830 ts_event,
831 ts_init,
832 false, venue_order_id,
834 Some(account_id),
835 price,
836 trigger_price,
837 protection_price,
838 false, ))
840}
841
842pub fn parse_execution_msg(
856 msg: BitmexExecutionMsg,
857 instrument: &InstrumentAny,
858 ts_init: UnixNanos,
859) -> Option<FillReport> {
860 let exec_type = msg.exec_type?;
861
862 match exec_type {
863 BitmexExecType::Trade | BitmexExecType::Liquidation => {}
865 BitmexExecType::Bankruptcy => {
866 log::warn!(
867 "Processing bankruptcy execution as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
868 msg.order_id,
869 msg.symbol,
870 );
871 }
872
873 BitmexExecType::Settlement => {
875 log::debug!(
876 "Settlement execution skipped (not a fill): applies quanto conversion/PnL transfer on contract settlement: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
877 msg.order_id,
878 msg.symbol,
879 );
880 return None;
881 }
882 BitmexExecType::TrialFill => {
883 log::warn!(
884 "Trial fill execution received (testnet only), not processed as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
885 msg.order_id,
886 msg.symbol,
887 );
888 return None;
889 }
890
891 BitmexExecType::Funding => {
893 log::debug!(
894 "Funding execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
895 msg.order_id,
896 msg.symbol,
897 );
898 return None;
899 }
900 BitmexExecType::Insurance => {
901 log::debug!(
902 "Insurance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
903 msg.order_id,
904 msg.symbol,
905 );
906 return None;
907 }
908 BitmexExecType::Rebalance => {
909 log::debug!(
910 "Rebalance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
911 msg.order_id,
912 msg.symbol,
913 );
914 return None;
915 }
916
917 BitmexExecType::New
919 | BitmexExecType::Canceled
920 | BitmexExecType::CancelReject
921 | BitmexExecType::Replaced
922 | BitmexExecType::Rejected
923 | BitmexExecType::AmendReject
924 | BitmexExecType::Suspended
925 | BitmexExecType::Released
926 | BitmexExecType::TriggeredOrActivatedBySystem => {
927 log::debug!(
928 "Execution message skipped (order state change, not a fill): exec_type={exec_type:?}, order_id={:?}",
929 msg.order_id,
930 );
931 return None;
932 }
933
934 BitmexExecType::Unknown(ref type_str) => {
935 log::warn!(
936 "Unknown execution type received, skipping: exec_type={type_str}, order_id={:?}, symbol={:?}",
937 msg.order_id,
938 msg.symbol,
939 );
940 return None;
941 }
942 }
943
944 let account_id = bitmex_account_id(msg.account?);
945 let instrument_id = parse_instrument_id(msg.symbol?);
946 let venue_order_id = VenueOrderId::new(msg.order_id?.to_string());
947 let trade_id = TradeId::new(msg.trd_match_id?.to_string());
948 let side = msg.side?;
949 let order_side = OrderSide::from(BitmexSide::from(side));
950 let last_qty = parse_signed_contracts_quantity(msg.last_qty?, instrument);
951 let last_px = Price::new(msg.last_px?, instrument.price_precision());
952 let settlement_currency_str = msg.settl_currency.unwrap_or(Ustr::from("XBT"));
953 let mapped_currency = map_bitmex_currency(settlement_currency_str.as_str());
954 let currency = get_currency(&mapped_currency);
955 let commission = Money::new(msg.commission.unwrap_or(0.0), currency);
956 let liquidity_side = parse_liquidity_side(&msg.last_liquidity_ind);
957 let client_order_id = msg.cl_ord_id.map(ClientOrderId::new);
958 let venue_position_id = None; let ts_event = parse_optional_datetime_to_unix_nanos(&msg.transact_time, "transact_time");
960
961 Some(FillReport::new(
962 account_id,
963 instrument_id,
964 venue_order_id,
965 trade_id,
966 order_side,
967 last_qty,
968 last_px,
969 commission,
970 liquidity_side,
971 client_order_id,
972 venue_position_id,
973 ts_event,
974 ts_init,
975 None,
976 ))
977}
978
979#[must_use]
985pub fn parse_position_msg(
986 msg: &BitmexPositionMsg,
987 instrument: &InstrumentAny,
988 ts_init: UnixNanos,
989) -> PositionStatusReport {
990 let account_id = bitmex_account_id(msg.account);
991 let instrument_id = parse_instrument_id(msg.symbol);
992 let position_side = parse_position_side(msg.current_qty);
993 let quantity = parse_signed_contracts_quantity(msg.current_qty.unwrap_or(0), instrument);
994 let venue_position_id = None; let avg_px_open = msg
996 .avg_entry_price
997 .and_then(|p| Decimal::from_str(&p.to_string()).ok());
998 let ts_last = parse_optional_datetime_to_unix_nanos(&msg.timestamp, "timestamp");
999
1000 PositionStatusReport::new(
1001 account_id,
1002 instrument_id,
1003 position_side,
1004 quantity,
1005 ts_last,
1006 ts_init,
1007 None, venue_position_id, avg_px_open, )
1011}
1012
1013#[must_use]
1026pub fn parse_instrument_msg(
1027 msg: &BitmexInstrumentMsg,
1028 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
1029 ts_init: UnixNanos,
1030) -> Vec<Data> {
1031 let mut updates = Vec::new();
1032 let is_index = is_index_symbol(&msg.symbol);
1033
1034 let effective_mark_price = msg.mark_price.or(msg.fair_price);
1037 let effective_index_price = if is_index {
1038 msg.last_price
1039 } else {
1040 msg.indicative_settle_price.or(msg.index_price)
1041 };
1042
1043 if effective_mark_price.is_none() && effective_index_price.is_none() {
1044 return updates;
1045 }
1046
1047 let instrument_id = InstrumentId::new(Symbol::from_ustr_unchecked(msg.symbol), *BITMEX_VENUE);
1048 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "");
1049
1050 let price_precision = match instruments_cache.get(&msg.symbol) {
1052 Some(instrument) => instrument.price_precision(),
1053 None => {
1054 if is_index {
1058 log::trace!(
1059 "Index instrument {} not in cache, skipping update",
1060 msg.symbol
1061 );
1062 } else {
1063 log::debug!("Instrument {} not in cache, skipping update", msg.symbol);
1064 }
1065 return updates;
1066 }
1067 };
1068
1069 if let Some(mark_price) = effective_mark_price {
1072 let price = Price::new(mark_price, price_precision);
1073 updates.push(Data::MarkPrice(MarkPriceUpdate::new(
1074 instrument_id,
1075 price,
1076 ts_event,
1077 ts_init,
1078 )));
1079 }
1080
1081 if let Some(index_price) = effective_index_price {
1083 let price = Price::new(index_price, price_precision);
1084 updates.push(Data::IndexPrice(IndexPriceUpdate::new(
1085 instrument_id,
1086 price,
1087 ts_event,
1088 ts_init,
1089 )));
1090 }
1091
1092 updates
1093}
1094
1095#[must_use]
1101pub fn parse_funding_msg(msg: &BitmexFundingMsg, ts_init: UnixNanos) -> FundingRateUpdate {
1102 let instrument_id = InstrumentId::from(format!("{}.BITMEX", msg.symbol));
1103 let funding_interval = Offset::UTC.to_datetime(msg.funding_interval);
1104 let interval_hours = u16::from(funding_interval.hour().cast_unsigned());
1105 let interval_minutes = u16::from(funding_interval.minute().cast_unsigned());
1106 let interval = Some(interval_hours * 60 + interval_minutes);
1107 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "");
1108
1109 FundingRateUpdate::new(
1110 instrument_id,
1111 msg.funding_rate,
1112 interval,
1113 None, ts_event,
1115 ts_init,
1116 )
1117}
1118
1119#[must_use]
1128pub fn parse_wallet_msg(msg: &BitmexWalletMsg, ts_init: UnixNanos) -> AccountState {
1129 let account_id = bitmex_account_id(msg.account);
1130
1131 let currency_str = map_bitmex_currency(msg.currency.as_str());
1133 let currency = get_currency(¤cy_str);
1134
1135 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1138 let amount_dec = Decimal::from(msg.amount.unwrap_or(0)) / divisor;
1139
1140 let balance = AccountBalance::from_total_and_locked(amount_dec, Decimal::ZERO, currency)
1141 .expect("Balance calculation should be valid");
1142
1143 AccountState::new(
1144 account_id,
1145 AccountType::Margin,
1146 vec![balance],
1147 vec![], true, UUID4::new(),
1150 ts_init,
1151 ts_init,
1152 None,
1153 )
1154}
1155
1156#[must_use]
1158pub fn parse_margin_msg(msg: &BitmexMarginMsg) -> MarginBalance {
1159 let currency_str = map_bitmex_currency(msg.currency.as_str());
1160 let currency = get_currency(¤cy_str);
1161
1162 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1163 let initial_dec = Decimal::from(msg.init_margin.unwrap_or(0).max(0)) / divisor;
1164 let maintenance_dec = Decimal::from(msg.maint_margin.unwrap_or(0).max(0)) / divisor;
1165
1166 MarginBalance::new(
1167 Money::from_decimal(initial_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1168 Money::from_decimal(maintenance_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1169 None,
1170 )
1171}
1172
1173#[must_use]
1175pub fn parse_margin_account_state(msg: &BitmexMarginMsg, ts_init: UnixNanos) -> AccountState {
1176 let account_id = bitmex_account_id(msg.account);
1177 let balance = parse_account_balance(msg);
1178
1179 let margin = parse_margin_msg(msg);
1180
1181 let margins = if !margin.initial.is_zero() || !margin.maintenance.is_zero() {
1182 vec![margin]
1183 } else {
1184 vec![]
1185 };
1186
1187 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "margin.timestamp");
1188
1189 AccountState::new(
1190 account_id,
1191 AccountType::Margin,
1192 vec![balance],
1193 margins,
1194 true,
1195 UUID4::new(),
1196 ts_event,
1197 ts_init,
1198 None,
1199 )
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204 use jiff::Timestamp;
1205 use nautilus_model::{
1206 enums::{AggressorSide, BookAction, LiquiditySide, PositionSide},
1207 identifiers::Symbol,
1208 instruments::crypto_perpetual::CryptoPerpetual,
1209 };
1210 use rstest::rstest;
1211 use ustr::Ustr;
1212
1213 use super::*;
1214 use crate::common::{
1215 enums::{BitmexExecType, BitmexOrderStatus},
1216 testing::load_test_json,
1217 };
1218
1219 fn create_test_perpetual_instrument_with_precisions(
1220 price_precision: u8,
1221 size_precision: u8,
1222 ) -> InstrumentAny {
1223 InstrumentAny::CryptoPerpetual(
1224 CryptoPerpetual::builder()
1225 .instrument_id(InstrumentId::from("XBTUSD.BITMEX"))
1226 .raw_symbol(Symbol::new("XBTUSD"))
1227 .base_currency(Currency::BTC())
1228 .quote_currency(Currency::USD())
1229 .settlement_currency(Currency::BTC())
1230 .is_inverse(true)
1231 .price_precision(price_precision)
1232 .size_precision(size_precision)
1233 .price_increment(Price::new(0.5, price_precision))
1234 .size_increment(Quantity::new(1.0, size_precision))
1235 .ts_event(UnixNanos::default())
1236 .ts_init(UnixNanos::default())
1237 .build()
1238 .unwrap(),
1239 )
1240 }
1241
1242 fn create_test_perpetual_instrument() -> InstrumentAny {
1243 create_test_perpetual_instrument_with_precisions(1, 0)
1244 }
1245
1246 #[rstest]
1247 fn test_orderbook_l2_message() {
1248 let json_data = load_test_json("ws_orderbook_l2.json");
1249
1250 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1251 let msg: BitmexOrderBookMsg = serde_json::from_str(&json_data).unwrap();
1252
1253 let instrument = create_test_perpetual_instrument();
1255
1256 let delta = parse_book_msg(
1258 &msg,
1259 &BitmexAction::Insert,
1260 &instrument,
1261 instrument.id(),
1262 instrument.price_precision(),
1263 UnixNanos::from(3),
1264 );
1265 assert_eq!(delta.instrument_id, instrument_id);
1266 assert_eq!(delta.order.price, Price::from("98459.9"));
1267 assert_eq!(delta.order.size, Quantity::from(33000));
1268 assert_eq!(delta.order.side, OrderSide::Sell.into());
1269 assert_eq!(delta.order.order_id, 62400580205);
1270 assert_eq!(delta.action, BookAction::Add);
1271 assert_eq!(delta.flags, 0);
1272 assert_eq!(delta.sequence, 0);
1273 assert_eq!(delta.ts_event, 1732436782356000000); assert_eq!(delta.ts_init, 3);
1275
1276 let delta = parse_book_msg(
1278 &msg,
1279 &BitmexAction::Partial,
1280 &instrument,
1281 instrument.id(),
1282 instrument.price_precision(),
1283 UnixNanos::from(3),
1284 );
1285 assert_eq!(delta.flags, RecordFlag::F_SNAPSHOT as u8);
1286 assert_eq!(delta.action, BookAction::Add);
1287
1288 let delta = parse_book_msg(
1290 &msg,
1291 &BitmexAction::Update,
1292 &instrument,
1293 instrument.id(),
1294 instrument.price_precision(),
1295 UnixNanos::from(3),
1296 );
1297 assert_eq!(delta.flags, 0);
1298 assert_eq!(delta.action, BookAction::Update);
1299 }
1300
1301 #[rstest]
1302 fn test_orderbook10_message() {
1303 let json_data = load_test_json("ws_orderbook_10.json");
1304 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1305 let msg: BitmexOrderBook10Msg = serde_json::from_str(&json_data).unwrap();
1306 let instrument = create_test_perpetual_instrument();
1307 let depth = parse_book10_msg(
1308 &msg,
1309 &instrument,
1310 instrument.id(),
1311 instrument.price_precision(),
1312 UnixNanos::from(3),
1313 )
1314 .unwrap();
1315
1316 assert_eq!(depth.instrument_id, instrument_id);
1317
1318 assert_eq!(depth.bids[0].price, Price::from("98490.3"));
1320 assert_eq!(depth.bids[0].size, Quantity::from(22400));
1321 assert_eq!(depth.bids[0].side, OrderSide::Buy.into());
1322
1323 assert_eq!(depth.asks[0].price, Price::from("98490.4"));
1325 assert_eq!(depth.asks[0].size, Quantity::from(17600));
1326 assert_eq!(depth.asks[0].side, OrderSide::Sell.into());
1327
1328 assert_eq!(depth.bid_counts.as_slice(), &[1; DEPTH10_LEN]);
1330 assert_eq!(depth.ask_counts.as_slice(), &[1; DEPTH10_LEN]);
1331
1332 assert_eq!(depth.sequence, 0);
1334 assert_eq!(depth.flags, RecordFlag::F_SNAPSHOT as u8);
1335 assert_eq!(depth.ts_event, 1732436353513000000); assert_eq!(depth.ts_init, 3);
1337 }
1338
1339 #[rstest]
1340 fn test_quote_message() {
1341 let json_data = load_test_json("ws_quote.json");
1342
1343 let instrument_id = InstrumentId::from("BCHUSDT.BITMEX");
1344 let last_quote = QuoteTick::new(
1345 instrument_id,
1346 Price::new(487.50, 2),
1347 Price::new(488.20, 2),
1348 Quantity::from(100_000),
1349 Quantity::from(100_000),
1350 UnixNanos::from(1),
1351 UnixNanos::from(2),
1352 );
1353 let msg: BitmexQuoteMsg = serde_json::from_str(&json_data).unwrap();
1354 let instrument = create_test_perpetual_instrument_with_precisions(2, 0);
1355 let quote = parse_quote_msg(
1356 &msg,
1357 &last_quote,
1358 &instrument,
1359 instrument_id,
1360 instrument.price_precision(),
1361 UnixNanos::from(3),
1362 );
1363
1364 assert_eq!(quote.instrument_id, instrument_id);
1365 assert_eq!(quote.bid_price, Price::from("487.55"));
1366 assert_eq!(quote.ask_price, Price::from("488.25"));
1367 assert_eq!(quote.bid_size, Quantity::from(103_000));
1368 assert_eq!(quote.ask_size, Quantity::from(50_000));
1369 assert_eq!(quote.ts_event, 1732315465085000000);
1370 assert_eq!(quote.ts_init, 3);
1371 }
1372
1373 #[rstest]
1374 fn test_trade_message() {
1375 let json_data = load_test_json("ws_trade.json");
1376
1377 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1378 let msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1379 let instrument = create_test_perpetual_instrument();
1380 let trade = parse_trade_msg(
1381 &msg,
1382 &instrument,
1383 instrument.id(),
1384 instrument.price_precision(),
1385 UnixNanos::from(3),
1386 );
1387
1388 assert_eq!(trade.instrument_id, instrument_id);
1389 assert_eq!(trade.price, Price::from("98570.9"));
1390 assert_eq!(trade.size, Quantity::from(100));
1391 assert_eq!(trade.aggressor_side, AggressorSide::Sell);
1392 assert_eq!(
1393 trade.trade_id.to_string(),
1394 "00000000-006d-1000-0000-000e8737d536"
1395 );
1396 assert_eq!(trade.ts_event, 1732436138704000000); assert_eq!(trade.ts_init, 3);
1398 }
1399
1400 #[rstest]
1401 fn test_trade_message_derives_trade_id_when_trd_match_id_missing() {
1402 let json_data = load_test_json("ws_trade.json");
1403 let mut msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1404 msg.trd_match_id = None;
1405 let instrument = create_test_perpetual_instrument();
1406
1407 let trade = parse_trade_msg(
1408 &msg,
1409 &instrument,
1410 instrument.id(),
1411 instrument.price_precision(),
1412 UnixNanos::from(3),
1413 );
1414
1415 let mut again_msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1416 again_msg.trd_match_id = None;
1417 let again = parse_trade_msg(
1418 &again_msg,
1419 &instrument,
1420 instrument.id(),
1421 instrument.price_precision(),
1422 UnixNanos::from(3),
1423 );
1424
1425 assert_eq!(trade.trade_id, again.trade_id, "derivation must be stable");
1426 assert_eq!(trade.trade_id.as_str().len(), 16);
1427
1428 let mut altered: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1429 altered.trd_match_id = None;
1430 altered.price += 1.0;
1431 let altered_trade = parse_trade_msg(
1432 &altered,
1433 &instrument,
1434 instrument.id(),
1435 instrument.price_precision(),
1436 UnixNanos::from(3),
1437 );
1438 assert_ne!(trade.trade_id, altered_trade.trade_id);
1439 }
1440
1441 #[rstest]
1442 fn test_trade_bin_message() {
1443 let json_data = load_test_json("ws_trade_bin_1m.json");
1444
1445 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1446 let topic = BitmexWsTopic::TradeBin1m;
1447
1448 let msg: BitmexTradeBinMsg = serde_json::from_str(&json_data).unwrap();
1449 let instrument = create_test_perpetual_instrument();
1450 let bar = parse_trade_bin_msg(
1451 &msg,
1452 &topic,
1453 &instrument,
1454 instrument.id(),
1455 instrument.price_precision(),
1456 UnixNanos::from(3),
1457 );
1458
1459 assert_eq!(bar.instrument_id(), instrument_id);
1460 assert_eq!(
1461 bar.bar_type.spec(),
1462 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1463 );
1464 assert_eq!(bar.open, Price::from("97550.0"));
1465 assert_eq!(bar.high, Price::from("97584.4"));
1466 assert_eq!(bar.low, Price::from("97550.0"));
1467 assert_eq!(bar.close, Price::from("97570.1"));
1468 assert_eq!(bar.volume, Quantity::from(84_000));
1469 assert_eq!(bar.ts_event, 1732392420000000000); assert_eq!(bar.ts_init, 3);
1471 }
1472
1473 #[rstest]
1474 fn test_trade_bin_message_extreme_adjustment() {
1475 let topic = BitmexWsTopic::TradeBin1m;
1476 let instrument = create_test_perpetual_instrument();
1477
1478 let msg = BitmexTradeBinMsg {
1479 timestamp: "2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap(),
1480 symbol: Ustr::from("XBTUSD"),
1481 open: 50_000.0,
1482 high: 49_990.0,
1483 low: 50_010.0,
1484 close: 50_005.0,
1485 trades: 10,
1486 volume: 1_000,
1487 vwap: Some(0.0),
1488 last_size: Some(0),
1489 turnover: 0,
1490 home_notional: 0.0,
1491 foreign_notional: 0.0,
1492 pool: None,
1493 };
1494
1495 let bar = parse_trade_bin_msg(
1496 &msg,
1497 &topic,
1498 &instrument,
1499 instrument.id(),
1500 instrument.price_precision(),
1501 UnixNanos::from(3),
1502 );
1503
1504 assert_eq!(bar.high, Price::from("50010.0"));
1505 assert_eq!(bar.low, Price::from("49990.0"));
1506 assert_eq!(bar.open, Price::from("50000.0"));
1507 assert_eq!(bar.close, Price::from("50005.0"));
1508 assert_eq!(bar.volume, Quantity::from(1_000));
1509 }
1510
1511 #[rstest]
1512 fn test_parse_order_msg() {
1513 let json_data = load_test_json("ws_order.json");
1514 let mut msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1515 msg.avg_px = Some(Decimal::from_str("30000.500000000004").unwrap());
1516 let mut cache = AHashMap::new();
1517 let instrument = create_test_perpetual_instrument();
1518 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1519
1520 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1521 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1522 assert_eq!(
1523 report.venue_order_id.to_string(),
1524 "550e8400-e29b-41d4-a716-446655440001"
1525 );
1526 assert_eq!(
1527 report.client_order_id.unwrap().to_string(),
1528 "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA"
1529 );
1530 assert_eq!(report.order_side, OrderSide::Buy.into());
1531 assert_eq!(report.order_type, OrderType::Limit);
1532 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1533 assert_eq!(report.order_status, OrderStatus::Accepted);
1534 assert_eq!(report.quantity, Quantity::from(100));
1535 assert_eq!(report.filled_qty, Quantity::from(0));
1536 assert_eq!(report.price.unwrap(), Price::from("98000.0"));
1537 assert_eq!(
1538 report.avg_px,
1539 Some(Decimal::from_str("30000.500000000004").unwrap())
1540 );
1541 assert_eq!(report.ts_accepted, 1732530600000000000); }
1543
1544 #[rstest]
1545 fn test_parse_order_msg_infers_type_when_missing() {
1546 let json_data = load_test_json("ws_order.json");
1547 let mut msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1548 msg.ord_type = None;
1549 msg.cl_ord_id = None;
1550 msg.price = Some(98_000.0);
1551 msg.stop_px = None;
1552
1553 let mut cache = AHashMap::new();
1554 let instrument = create_test_perpetual_instrument();
1555
1556 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1557
1558 assert_eq!(report.order_type, OrderType::Limit);
1559 }
1560
1561 #[rstest]
1562 fn test_parse_order_msg_rejected_with_reason() {
1563 let mut msg: BitmexOrderMsg =
1564 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1565 msg.ord_status = BitmexOrderStatus::Rejected;
1566 msg.ord_rej_reason = Some(Ustr::from("Insufficient available balance"));
1567 msg.text = None;
1568 msg.cum_qty = 0;
1569
1570 let mut cache = AHashMap::new();
1571 let instrument = create_test_perpetual_instrument();
1572 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1573
1574 assert_eq!(report.order_status, OrderStatus::Rejected);
1575 assert_eq!(
1576 report.cancel_reason,
1577 Some("Insufficient available balance".to_string())
1578 );
1579 }
1580
1581 #[rstest]
1582 fn test_parse_order_msg_rejected_with_text_fallback() {
1583 let mut msg: BitmexOrderMsg =
1584 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1585 msg.ord_status = BitmexOrderStatus::Rejected;
1586 msg.ord_rej_reason = None;
1587 msg.text = Some(Ustr::from("Order would execute immediately"));
1588 msg.cum_qty = 0;
1589
1590 let mut cache = AHashMap::new();
1591 let instrument = create_test_perpetual_instrument();
1592 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1593
1594 assert_eq!(report.order_status, OrderStatus::Rejected);
1595 assert_eq!(
1596 report.cancel_reason,
1597 Some("Order would execute immediately".to_string())
1598 );
1599 }
1600
1601 #[rstest]
1602 fn test_parse_order_msg_rejected_without_reason() {
1603 let mut msg: BitmexOrderMsg =
1604 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1605 msg.ord_status = BitmexOrderStatus::Rejected;
1606 msg.ord_rej_reason = None;
1607 msg.text = None;
1608 msg.cum_qty = 0;
1609
1610 let mut cache = AHashMap::new();
1611 let instrument = create_test_perpetual_instrument();
1612 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1613
1614 assert_eq!(report.order_status, OrderStatus::Rejected);
1615 assert_eq!(report.cancel_reason, None);
1616 }
1617
1618 #[rstest]
1619 fn test_parse_execution_msg() {
1620 let json_data = load_test_json("ws_execution.json");
1621 let msg: BitmexExecutionMsg = serde_json::from_str(&json_data).unwrap();
1622 let instrument = create_test_perpetual_instrument();
1623 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1624
1625 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1626 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1627 assert_eq!(
1628 fill.venue_order_id.to_string(),
1629 "550e8400-e29b-41d4-a716-446655440002"
1630 );
1631 assert_eq!(
1632 fill.trade_id.to_string(),
1633 "00000000-006d-1000-0000-000e8737d540"
1634 );
1635 assert_eq!(
1636 fill.client_order_id.unwrap().to_string(),
1637 "mm_bitmex_2b/oemUeQ4CAJZgP3fjHsB"
1638 );
1639 assert_eq!(fill.order_side, OrderSide::Sell);
1640 assert_eq!(fill.last_qty, Quantity::from(100));
1641 assert_eq!(fill.last_px, Price::from("98950.0"));
1642 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
1643 assert_eq!(fill.commission, Money::new(0.00075, Currency::from("XBT")));
1644 assert_eq!(fill.commission.currency.code.to_string(), "XBT");
1645 assert_eq!(fill.ts_event, 1732530900789000000); }
1647
1648 #[rstest]
1649 fn test_parse_execution_msg_non_trade() {
1650 let mut msg: BitmexExecutionMsg =
1652 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1653 msg.exec_type = Some(BitmexExecType::Settlement);
1654
1655 let instrument = create_test_perpetual_instrument();
1656 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1657 assert!(result.is_none());
1658 }
1659
1660 #[rstest]
1661 fn test_parse_cancel_reject_execution() {
1662 let json = load_test_json("ws_execution_cancel_reject.json");
1664
1665 let msg: BitmexExecutionMsg = serde_json::from_str(&json).unwrap();
1666 assert_eq!(msg.exec_type, Some(BitmexExecType::CancelReject));
1667 assert_eq!(msg.ord_status, Some(BitmexOrderStatus::Rejected));
1668 assert_eq!(msg.symbol, None);
1669
1670 let instrument = create_test_perpetual_instrument();
1672 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1673 assert!(result.is_none());
1674 }
1675
1676 #[rstest]
1677 fn test_parse_execution_msg_liquidation() {
1678 let mut msg: BitmexExecutionMsg =
1680 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1681 msg.exec_type = Some(BitmexExecType::Liquidation);
1682
1683 let instrument = create_test_perpetual_instrument();
1684 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1685
1686 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1687 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1688 assert_eq!(fill.order_side, OrderSide::Sell);
1689 assert_eq!(fill.last_qty, Quantity::from(100));
1690 assert_eq!(fill.last_px, Price::from("98950.0"));
1691 }
1692
1693 #[rstest]
1694 fn test_parse_execution_msg_bankruptcy() {
1695 let mut msg: BitmexExecutionMsg =
1696 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1697 msg.exec_type = Some(BitmexExecType::Bankruptcy);
1698
1699 let instrument = create_test_perpetual_instrument();
1700 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1701
1702 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1703 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1704 assert_eq!(fill.order_side, OrderSide::Sell);
1705 assert_eq!(fill.last_qty, Quantity::from(100));
1706 }
1707
1708 #[rstest]
1709 fn test_parse_execution_msg_settlement() {
1710 let mut msg: BitmexExecutionMsg =
1711 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1712 msg.exec_type = Some(BitmexExecType::Settlement);
1713
1714 let instrument = create_test_perpetual_instrument();
1715 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1716 assert!(result.is_none());
1717 }
1718
1719 #[rstest]
1720 fn test_parse_execution_msg_trial_fill() {
1721 let mut msg: BitmexExecutionMsg =
1722 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1723 msg.exec_type = Some(BitmexExecType::TrialFill);
1724
1725 let instrument = create_test_perpetual_instrument();
1726 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1727 assert!(result.is_none());
1728 }
1729
1730 #[rstest]
1731 fn test_parse_execution_msg_funding() {
1732 let mut msg: BitmexExecutionMsg =
1733 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1734 msg.exec_type = Some(BitmexExecType::Funding);
1735
1736 let instrument = create_test_perpetual_instrument();
1737 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1738 assert!(result.is_none());
1739 }
1740
1741 #[rstest]
1742 fn test_parse_execution_msg_insurance() {
1743 let mut msg: BitmexExecutionMsg =
1744 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1745 msg.exec_type = Some(BitmexExecType::Insurance);
1746
1747 let instrument = create_test_perpetual_instrument();
1748 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1749 assert!(result.is_none());
1750 }
1751
1752 #[rstest]
1753 fn test_parse_execution_msg_rebalance() {
1754 let mut msg: BitmexExecutionMsg =
1755 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1756 msg.exec_type = Some(BitmexExecType::Rebalance);
1757
1758 let instrument = create_test_perpetual_instrument();
1759 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1760 assert!(result.is_none());
1761 }
1762
1763 #[rstest]
1764 fn test_parse_execution_msg_order_state_changes() {
1765 let instrument = create_test_perpetual_instrument();
1766
1767 let order_state_types = vec![
1768 BitmexExecType::New,
1769 BitmexExecType::Canceled,
1770 BitmexExecType::CancelReject,
1771 BitmexExecType::Replaced,
1772 BitmexExecType::Rejected,
1773 BitmexExecType::AmendReject,
1774 BitmexExecType::Suspended,
1775 BitmexExecType::Released,
1776 BitmexExecType::TriggeredOrActivatedBySystem,
1777 ];
1778
1779 for exec_type in order_state_types {
1780 let mut msg: BitmexExecutionMsg =
1781 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1782 msg.exec_type = Some(exec_type.clone());
1783
1784 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1785 assert!(
1786 result.is_none(),
1787 "Expected None for exec_type {exec_type:?}"
1788 );
1789 }
1790 }
1791
1792 #[rstest]
1793 fn test_parse_position_msg() {
1794 let json_data = load_test_json("ws_position.json");
1795 let msg: BitmexPositionMsg = serde_json::from_str(&json_data).unwrap();
1796 let instrument = create_test_perpetual_instrument();
1797 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1798
1799 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1800 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1801 assert_eq!(report.position_side, PositionSide::Long);
1802 assert_eq!(report.quantity, Quantity::from(1000));
1803 assert!(report.venue_position_id.is_none());
1804 assert_eq!(report.ts_last, 1732530900789000000); }
1806
1807 #[rstest]
1808 fn test_parse_position_msg_short() {
1809 let mut msg: BitmexPositionMsg =
1810 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1811 msg.current_qty = Some(-500);
1812
1813 let instrument = create_test_perpetual_instrument();
1814 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1815 assert_eq!(report.position_side, PositionSide::Short);
1816 assert_eq!(report.quantity, Quantity::from(500));
1817 }
1818
1819 #[rstest]
1820 fn test_parse_position_msg_flat() {
1821 let mut msg: BitmexPositionMsg =
1822 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1823 msg.current_qty = Some(0);
1824
1825 let instrument = create_test_perpetual_instrument();
1826 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1827 assert_eq!(report.position_side, PositionSide::Flat);
1828 assert_eq!(report.quantity, Quantity::from(0));
1829 }
1830
1831 #[rstest]
1832 fn test_parse_wallet_msg() {
1833 let json_data = load_test_json("ws_wallet.json");
1834 let msg: BitmexWalletMsg = serde_json::from_str(&json_data).unwrap();
1835 let ts_init = UnixNanos::from(1);
1836 let account_state = parse_wallet_msg(&msg, ts_init);
1837
1838 assert_eq!(account_state.account_id.to_string(), "BITMEX-1234567");
1839 assert!(!account_state.balances.is_empty());
1840 let balance = &account_state.balances[0];
1841 assert_eq!(balance.currency.code.to_string(), "XBT");
1842 assert!((balance.total.as_f64() - 1.0000518).abs() < 1e-7);
1844 assert_eq!(balance.locked.as_f64(), 0.0);
1846 assert_eq!(balance.free.as_decimal(), balance.total.as_decimal());
1847 }
1848
1849 #[rstest]
1850 fn test_parse_wallet_msg_no_amount() {
1851 let mut msg: BitmexWalletMsg =
1852 serde_json::from_str(&load_test_json("ws_wallet.json")).unwrap();
1853 msg.amount = None;
1854
1855 let ts_init = UnixNanos::from(1);
1856 let account_state = parse_wallet_msg(&msg, ts_init);
1857 let balance = &account_state.balances[0];
1858 assert_eq!(balance.total.as_f64(), 0.0);
1859 }
1860
1861 #[rstest]
1862 fn test_parse_margin_msg() {
1863 let json_data = load_test_json("ws_margin.json");
1864 let msg: BitmexMarginMsg = serde_json::from_str(&json_data).unwrap();
1865 let margin_balance = parse_margin_msg(&msg);
1866
1867 assert_eq!(margin_balance.currency.code.to_string(), "XBT");
1868 assert!(margin_balance.instrument_id.is_none());
1869 assert_eq!(margin_balance.initial.as_f64(), 0.0);
1872 assert!((margin_balance.maintenance.as_f64() - 0.00015949).abs() < 1e-8);
1874 }
1875
1876 #[rstest]
1877 fn test_parse_margin_msg_no_available() {
1878 let mut msg: BitmexMarginMsg =
1879 serde_json::from_str(&load_test_json("ws_margin.json")).unwrap();
1880 msg.available_margin = None;
1881
1882 let margin_balance = parse_margin_msg(&msg);
1883 assert!(margin_balance.initial.as_f64() >= 0.0);
1885 assert!(margin_balance.maintenance.as_f64() >= 0.0);
1886 }
1887
1888 #[rstest]
1889 fn test_parse_margin_account_state_includes_margins() {
1890 let msg = BitmexMarginMsg {
1891 account: 123456,
1892 currency: Ustr::from("USDt"),
1893 risk_limit: None,
1894 amount: Some(5_000_000_000),
1895 prev_realised_pnl: None,
1896 gross_comm: None,
1897 gross_open_cost: None,
1898 gross_open_premium: None,
1899 gross_exec_cost: None,
1900 gross_mark_value: None,
1901 risk_value: None,
1902 init_margin: Some(200_000_000), maint_margin: Some(100_000_000), target_excess_margin: None,
1905 realised_pnl: None,
1906 unrealised_pnl: None,
1907 wallet_balance: Some(5_000_000_000), margin_balance: None,
1909 margin_leverage: None,
1910 margin_used_pcnt: None,
1911 excess_margin: None,
1912 available_margin: Some(4_800_000_000), withdrawable_margin: None,
1914 maker_fee_discount: None,
1915 taker_fee_discount: None,
1916 timestamp: Timestamp::from_second(1_700_000_000).unwrap(),
1917 foreign_margin_balance: None,
1918 foreign_requirement: None,
1919 };
1920
1921 let ts_init = UnixNanos::from(1_000_000_000u64);
1922 let state = parse_margin_account_state(&msg, ts_init);
1923
1924 assert_eq!(state.account_id.to_string(), "BITMEX-123456");
1925 assert_eq!(state.account_type, AccountType::Margin);
1926 assert_eq!(state.balances.len(), 1);
1927 assert_eq!(state.margins.len(), 1);
1928
1929 let balance = &state.balances[0];
1930 assert_eq!(balance.total.as_f64(), 5000.0);
1931
1932 let margin = &state.margins[0];
1933 assert!(margin.instrument_id.is_none());
1934 assert_eq!(margin.currency.code, "USDT");
1935 assert_eq!(margin.initial.as_f64(), 200.0);
1936 assert_eq!(margin.maintenance.as_f64(), 100.0);
1937 }
1938
1939 #[rstest]
1940 fn test_parse_margin_account_state_zero_margins_excluded() {
1941 let msg = BitmexMarginMsg {
1942 account: 123456,
1943 currency: Ustr::from("XBt"),
1944 risk_limit: None,
1945 amount: Some(100_000_000),
1946 prev_realised_pnl: None,
1947 gross_comm: None,
1948 gross_open_cost: None,
1949 gross_open_premium: None,
1950 gross_exec_cost: None,
1951 gross_mark_value: None,
1952 risk_value: None,
1953 init_margin: Some(0),
1954 maint_margin: Some(0),
1955 target_excess_margin: None,
1956 realised_pnl: None,
1957 unrealised_pnl: None,
1958 wallet_balance: Some(100_000_000),
1959 margin_balance: None,
1960 margin_leverage: None,
1961 margin_used_pcnt: None,
1962 excess_margin: None,
1963 available_margin: Some(100_000_000),
1964 withdrawable_margin: None,
1965 maker_fee_discount: None,
1966 taker_fee_discount: None,
1967 timestamp: Timestamp::from_second(1_700_000_000).unwrap(),
1968 foreign_margin_balance: None,
1969 foreign_requirement: None,
1970 };
1971
1972 let state = parse_margin_account_state(&msg, UnixNanos::from(1_000_000_000u64));
1973
1974 assert_eq!(state.balances.len(), 1);
1975 assert_eq!(state.margins.len(), 0);
1976 }
1977
1978 #[rstest]
1979 fn test_parse_instrument_msg_both_prices() {
1980 let json_data = load_test_json("ws_instrument.json");
1981 let msg: BitmexInstrumentMsg = serde_json::from_str(&json_data).unwrap();
1982
1983 let mut instruments_cache = AHashMap::new();
1985 let test_instrument = create_test_perpetual_instrument();
1986 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
1987
1988 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
1989
1990 assert_eq!(updates.len(), 2);
1992
1993 match &updates[0] {
1994 Data::MarkPrice(update) => {
1995 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
1996 assert_eq!(update.value.as_f64(), 95125.7);
1997 }
1998 _ => panic!("Expected MarkPriceUpdate at index 0"),
1999 }
2000
2001 match &updates[1] {
2002 Data::IndexPrice(update) => {
2003 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2004 assert_eq!(update.value.as_f64(), 95126.0);
2005 }
2006 _ => panic!("Expected IndexPriceUpdate at index 1"),
2007 }
2008 }
2009
2010 #[rstest]
2011 fn test_parse_instrument_msg_mark_price_only() {
2012 let mut msg: BitmexInstrumentMsg =
2013 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2014 msg.index_price = None;
2015 msg.indicative_settle_price = None;
2016
2017 let mut instruments_cache = AHashMap::new();
2018 let test_instrument = create_test_perpetual_instrument();
2019 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2020
2021 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2022
2023 assert_eq!(updates.len(), 1);
2024 match &updates[0] {
2025 Data::MarkPrice(update) => {
2026 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2027 assert_eq!(update.value.as_f64(), 95125.7);
2028 }
2029 _ => panic!("Expected MarkPriceUpdate"),
2030 }
2031 }
2032
2033 #[rstest]
2034 fn test_parse_instrument_msg_index_price_only() {
2035 let mut msg: BitmexInstrumentMsg =
2036 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2037 msg.mark_price = None;
2038 msg.fair_price = None;
2039
2040 let mut instruments_cache = AHashMap::new();
2041 let test_instrument = create_test_perpetual_instrument();
2042 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2043
2044 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2045
2046 assert_eq!(updates.len(), 1);
2047 match &updates[0] {
2048 Data::IndexPrice(update) => {
2049 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2050 assert_eq!(update.value.as_f64(), 95126.0);
2051 }
2052 _ => panic!("Expected IndexPriceUpdate"),
2053 }
2054 }
2055
2056 #[rstest]
2057 fn test_parse_instrument_msg_no_prices() {
2058 let mut msg: BitmexInstrumentMsg =
2059 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2060 msg.mark_price = None;
2061 msg.fair_price = None;
2062 msg.index_price = None;
2063 msg.indicative_settle_price = None;
2064 msg.last_price = None;
2065
2066 let mut instruments_cache = AHashMap::new();
2068 let test_instrument = create_test_perpetual_instrument();
2069 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2070
2071 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2072 assert_eq!(updates.len(), 0);
2073 }
2074
2075 #[rstest]
2076 fn test_parse_instrument_msg_index_symbol() {
2077 let mut msg: BitmexInstrumentMsg =
2080 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2081 msg.symbol = Ustr::from(".BXBT");
2082 msg.last_price = Some(119163.05);
2083 msg.mark_price = Some(119163.05); msg.fair_price = None;
2085 msg.index_price = None;
2086 msg.indicative_settle_price = None;
2087
2088 let instrument_id = InstrumentId::from(".BXBT.BITMEX");
2090 let instrument = CryptoPerpetual::builder()
2091 .instrument_id(instrument_id)
2092 .raw_symbol(Symbol::from(".BXBT"))
2093 .base_currency(Currency::BTC())
2094 .quote_currency(Currency::USD())
2095 .settlement_currency(Currency::USD())
2096 .is_inverse(false)
2097 .price_precision(2)
2099 .size_precision(8)
2100 .price_increment(Price::from("0.01"))
2101 .size_increment(Quantity::from("0.00000001"))
2102 .ts_event(UnixNanos::default())
2103 .ts_init(UnixNanos::default())
2104 .build()
2105 .unwrap();
2106 let mut instruments_cache = AHashMap::new();
2107 instruments_cache.insert(
2108 Ustr::from(".BXBT"),
2109 InstrumentAny::CryptoPerpetual(instrument),
2110 );
2111
2112 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2113
2114 assert_eq!(updates.len(), 2);
2115
2116 match &updates[0] {
2118 Data::MarkPrice(update) => {
2119 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2120 assert_eq!(update.value, Price::from("119163.05"));
2121 }
2122 _ => panic!("Expected MarkPriceUpdate for index symbol"),
2123 }
2124
2125 match &updates[1] {
2127 Data::IndexPrice(update) => {
2128 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2129 assert_eq!(update.value, Price::from("119163.05"));
2130 assert_eq!(update.ts_init, UnixNanos::from(1));
2131 }
2132 _ => panic!("Expected IndexPriceUpdate for index symbol"),
2133 }
2134 }
2135
2136 #[rstest]
2138 fn test_parse_instrument_msg_mark_update_wire_shape() {
2139 let msg: BitmexInstrumentMsg =
2140 serde_json::from_str(&load_test_json("ws_instrument_mark_update.json")).unwrap();
2141
2142 let instrument_id = InstrumentId::from("DOTUSDT.BITMEX");
2143 let instrument = CryptoPerpetual::builder()
2144 .instrument_id(instrument_id)
2145 .raw_symbol(Symbol::from("DOTUSDT"))
2146 .base_currency(Currency::from_str("DOT").unwrap())
2147 .quote_currency(Currency::USDT())
2148 .settlement_currency(Currency::USDT())
2149 .is_inverse(false)
2150 .price_precision(4)
2152 .size_precision(8)
2153 .price_increment(Price::from("0.0001"))
2154 .size_increment(Quantity::from("0.00000001"))
2155 .ts_event(UnixNanos::default())
2156 .ts_init(UnixNanos::default())
2157 .build()
2158 .unwrap();
2159 let mut instruments_cache = AHashMap::new();
2160 instruments_cache.insert(
2161 Ustr::from("DOTUSDT"),
2162 InstrumentAny::CryptoPerpetual(instrument),
2163 );
2164
2165 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2166
2167 assert_eq!(updates.len(), 1);
2168 match &updates[0] {
2169 Data::MarkPrice(update) => {
2170 assert_eq!(update.instrument_id.to_string(), "DOTUSDT.BITMEX");
2171 assert_eq!(update.value, Price::from("1.2669"));
2172 }
2173 _ => panic!("Expected single MarkPriceUpdate for mark-update wire shape"),
2174 }
2175 }
2176
2177 #[rstest]
2179 fn test_parse_instrument_msg_index_update_wire_shape() {
2180 let msg: BitmexInstrumentMsg =
2181 serde_json::from_str(&load_test_json("ws_instrument_index_update.json")).unwrap();
2182
2183 let mut instruments_cache = AHashMap::new();
2184 instruments_cache.insert(
2185 Ustr::from("XBTUSD"),
2186 create_test_perpetual_instrument_with_precisions(2, 0),
2187 );
2188
2189 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2190
2191 assert_eq!(updates.len(), 1);
2192 match &updates[0] {
2193 Data::IndexPrice(update) => {
2194 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2195 assert_eq!(update.value, Price::from("75847.62"));
2196 }
2197 _ => panic!("Expected single IndexPriceUpdate for index-update wire shape"),
2198 }
2199 }
2200
2201 #[rstest]
2202 fn test_parse_funding_msg() {
2203 let json_data = load_test_json("ws_funding_rate.json");
2204 let msg: BitmexFundingMsg = serde_json::from_str(&json_data).unwrap();
2205 let update = parse_funding_msg(&msg, UnixNanos::from(1));
2206
2207 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2208 assert_eq!(update.rate.to_string(), "0.0001");
2209 assert_eq!(update.interval, Some(60 * 8));
2210 assert!(update.next_funding_ns.is_none());
2211 assert_eq!(update.ts_event, UnixNanos::from(1732507200000000000));
2212 assert_eq!(update.ts_init, UnixNanos::from(1));
2213 }
2214}