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, OrderBookDepth10, QuoteTick, TradeTick,
29 depth::DEPTH10_LEN,
30 },
31 enums::{
32 AccountType, AggregationSource, BarAggregation, OrderSide, OrderStatus, OrderType,
33 PriceType, RecordFlag, TimeInForce, TrailingOffsetType,
34 },
35 events::{
36 OrderAccepted, OrderCanceled, OrderExpired, OrderRejected, OrderTriggered, OrderUpdated,
37 account::state::AccountState,
38 },
39 identifiers::{
40 AccountId, ClientOrderId, InstrumentId, OrderListId, StrategyId, Symbol, TradeId, TraderId,
41 VenueOrderId,
42 },
43 instruments::{Instrument, InstrumentAny},
44 reports::{FillReport, OrderStatusReport, PositionStatusReport},
45 types::{AccountBalance, MarginBalance, Money, Price, Quantity},
46};
47use rust_decimal::Decimal;
48use ustr::Ustr;
49
50use super::{
51 enums::{BitmexAction, BitmexWsTopic},
52 messages::{
53 BitmexExecutionMsg, BitmexFundingMsg, BitmexInstrumentMsg, BitmexMarginMsg,
54 BitmexOrderBook10Msg, BitmexOrderBookMsg, BitmexOrderMsg, BitmexPositionMsg,
55 BitmexQuoteMsg, BitmexTradeBinMsg, BitmexTradeMsg, BitmexWalletMsg,
56 },
57};
58use crate::{
59 common::{
60 consts::BITMEX_VENUE,
61 enums::{
62 BitmexExecInstruction, BitmexExecType, BitmexOrderStatus, BitmexOrderType,
63 BitmexPegPriceType, BitmexSide,
64 },
65 parse::{
66 bitmex_account_id, bitmex_currency_divisor, clean_reason, derive_trade_id,
67 extract_trigger_type, map_bitmex_currency, normalize_trade_bin_prices,
68 normalize_trade_bin_volume, parse_account_balance, parse_contracts_quantity,
69 parse_fractional_quantity, parse_instrument_id, parse_liquidity_side,
70 parse_optional_datetime_to_unix_nanos, parse_position_side,
71 parse_signed_contracts_quantity,
72 },
73 },
74 http::parse::get_currency,
75 websocket::messages::BitmexOrderUpdateMsg,
76};
77
78const BAR_SPEC_1_MINUTE: BarSpecification = BarSpecification {
79 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
80 aggregation: BarAggregation::Minute,
81 price_type: PriceType::Last,
82};
83const BAR_SPEC_5_MINUTE: BarSpecification = BarSpecification {
84 step: NonZero::new(5).expect("5 is a valid non-zero usize"),
85 aggregation: BarAggregation::Minute,
86 price_type: PriceType::Last,
87};
88const BAR_SPEC_1_HOUR: BarSpecification = BarSpecification {
89 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
90 aggregation: BarAggregation::Hour,
91 price_type: PriceType::Last,
92};
93const BAR_SPEC_1_DAY: BarSpecification = BarSpecification {
94 step: NonZero::new(1).expect("1 is a valid non-zero usize"),
95 aggregation: BarAggregation::Day,
96 price_type: PriceType::Last,
97};
98
99#[inline]
107#[must_use]
108pub fn is_index_symbol(symbol: &Ustr) -> bool {
109 symbol.starts_with('.')
110}
111
112#[must_use]
114pub fn parse_book_msg_vec(
115 data: Vec<BitmexOrderBookMsg>,
116 action: BitmexAction,
117 instruments: &AHashMap<Ustr, InstrumentAny>,
118 ts_init: UnixNanos,
119) -> Vec<Data> {
120 let mut deltas = Vec::with_capacity(data.len());
121
122 for msg in data {
123 if let Some(instrument) = instruments.get(&msg.symbol) {
124 let instrument_id = instrument.id();
125 let price_precision = instrument.price_precision();
126 deltas.push(Data::Delta(parse_book_msg(
127 &msg,
128 &action,
129 instrument,
130 instrument_id,
131 price_precision,
132 ts_init,
133 )));
134 } else {
135 log::error!(
136 "Instrument cache miss: book delta dropped for symbol={}",
137 msg.symbol
138 );
139 }
140 }
141
142 if let Some(Data::Delta(last_delta)) = deltas.last_mut() {
144 *last_delta = OrderBookDelta::new(
145 last_delta.instrument_id,
146 last_delta.action,
147 last_delta.order,
148 last_delta.flags | RecordFlag::F_LAST as u8,
149 last_delta.sequence,
150 last_delta.ts_event,
151 last_delta.ts_init,
152 );
153 }
154
155 deltas
156}
157
158#[must_use]
160pub fn parse_book10_msg_vec(
161 data: Vec<BitmexOrderBook10Msg>,
162 instruments: &AHashMap<Ustr, InstrumentAny>,
163 ts_init: UnixNanos,
164) -> Vec<Data> {
165 let mut depths = Vec::with_capacity(data.len());
166
167 for msg in data {
168 if let Some(instrument) = instruments.get(&msg.symbol) {
169 let instrument_id = instrument.id();
170 let price_precision = instrument.price_precision();
171 match parse_book10_msg(&msg, instrument, instrument_id, price_precision, ts_init) {
172 Ok(depth) => depths.push(Data::Depth10(Box::new(depth))),
173 Err(e) => {
174 log::error!("Failed to parse orderBook10 for symbol={}: {e}", msg.symbol);
175 }
176 }
177 } else {
178 log::error!(
179 "Instrument cache miss: depth10 message dropped for symbol={}",
180 msg.symbol
181 );
182 }
183 }
184 depths
185}
186
187#[must_use]
189pub fn parse_trade_msg_vec(
190 data: Vec<BitmexTradeMsg>,
191 instruments: &AHashMap<Ustr, InstrumentAny>,
192 ts_init: UnixNanos,
193) -> Vec<Data> {
194 let mut trades = Vec::with_capacity(data.len());
195
196 for msg in data {
197 if let Some(instrument) = instruments.get(&msg.symbol) {
198 let instrument_id = instrument.id();
199 let price_precision = instrument.price_precision();
200 trades.push(Data::Trade(parse_trade_msg(
201 &msg,
202 instrument,
203 instrument_id,
204 price_precision,
205 ts_init,
206 )));
207 } else {
208 log::error!(
209 "Instrument cache miss: trade message dropped for symbol={}",
210 msg.symbol
211 );
212 }
213 }
214 trades
215}
216
217#[must_use]
219pub fn parse_trade_bin_msg_vec(
220 data: Vec<BitmexTradeBinMsg>,
221 topic: &BitmexWsTopic,
222 instruments: &AHashMap<Ustr, InstrumentAny>,
223 ts_init: UnixNanos,
224) -> Vec<Data> {
225 let mut trades = Vec::with_capacity(data.len());
226
227 for msg in data {
228 if let Some(instrument) = instruments.get(&msg.symbol) {
229 let instrument_id = instrument.id();
230 let price_precision = instrument.price_precision();
231 trades.push(Data::Bar(parse_trade_bin_msg(
232 &msg,
233 topic,
234 instrument,
235 instrument_id,
236 price_precision,
237 ts_init,
238 )));
239 } else {
240 log::error!(
241 "Instrument cache miss: trade bin (bar) dropped for symbol={}",
242 msg.symbol
243 );
244 }
245 }
246 trades
247}
248
249#[must_use]
251pub fn parse_book_msg(
252 msg: &BitmexOrderBookMsg,
253 action: &BitmexAction,
254 instrument: &InstrumentAny,
255 instrument_id: InstrumentId,
256 price_precision: u8,
257 ts_init: UnixNanos,
258) -> OrderBookDelta {
259 let flags = if action == &BitmexAction::Partial {
260 RecordFlag::F_SNAPSHOT as u8
261 } else {
262 0
263 };
264
265 let action = action.as_book_action();
266 let price = Price::new(msg.price, price_precision);
267 let side = msg.side.as_order_side();
268 let size = parse_contracts_quantity(msg.size.unwrap_or(0), instrument);
269 let order_id = msg.id;
270 let order = BookOrder::new(side, price, size, order_id);
271 let sequence = 0; let ts_event = UnixNanos::from(msg.timestamp);
273
274 OrderBookDelta::new(
275 instrument_id,
276 action,
277 order,
278 flags,
279 sequence,
280 ts_event,
281 ts_init,
282 )
283}
284
285pub fn parse_book10_msg(
291 msg: &BitmexOrderBook10Msg,
292 instrument: &InstrumentAny,
293 instrument_id: InstrumentId,
294 price_precision: u8,
295 ts_init: UnixNanos,
296) -> anyhow::Result<OrderBookDepth10> {
297 let mut bids = Vec::with_capacity(DEPTH10_LEN);
298 let mut asks = Vec::with_capacity(DEPTH10_LEN);
299
300 let mut bid_counts: [u32; DEPTH10_LEN] = [0; DEPTH10_LEN];
302 let mut ask_counts: [u32; DEPTH10_LEN] = [0; DEPTH10_LEN];
303
304 for (i, level) in msg.bids.iter().enumerate() {
305 let bid_order = BookOrder::new(
306 OrderSide::Buy,
307 Price::new(level[0], price_precision),
308 parse_fractional_quantity(level[1], instrument),
309 0,
310 );
311
312 bids.push(bid_order);
313 bid_counts[i] = 1;
314 }
315
316 for (i, level) in msg.asks.iter().enumerate() {
317 let ask_order = BookOrder::new(
318 OrderSide::Sell,
319 Price::new(level[0], price_precision),
320 parse_fractional_quantity(level[1], instrument),
321 0,
322 );
323
324 asks.push(ask_order);
325 ask_counts[i] = 1;
326 }
327
328 let bids: [BookOrder; DEPTH10_LEN] = bids.try_into().map_err(|v: Vec<BookOrder>| {
329 anyhow::anyhow!(
330 "Bids length mismatch: expected {DEPTH10_LEN}, was {}",
331 v.len()
332 )
333 })?;
334 let asks: [BookOrder; DEPTH10_LEN] = asks.try_into().map_err(|v: Vec<BookOrder>| {
335 anyhow::anyhow!(
336 "Asks length mismatch: expected {DEPTH10_LEN}, was {}",
337 v.len()
338 )
339 })?;
340
341 let ts_event = UnixNanos::from(msg.timestamp);
342
343 Ok(OrderBookDepth10::new(
344 instrument_id,
345 bids,
346 asks,
347 bid_counts,
348 ask_counts,
349 RecordFlag::F_SNAPSHOT as u8,
350 0, ts_event,
352 ts_init,
353 ))
354}
355
356#[must_use]
358pub fn parse_quote_msg(
359 msg: &BitmexQuoteMsg,
360 last_quote: &QuoteTick,
361 instrument: &InstrumentAny,
362 instrument_id: InstrumentId,
363 price_precision: u8,
364 ts_init: UnixNanos,
365) -> QuoteTick {
366 let bid_price = match msg.bid_price {
367 Some(price) => Price::new(price, price_precision),
368 None => last_quote.bid_price,
369 };
370
371 let ask_price = match msg.ask_price {
372 Some(price) => Price::new(price, price_precision),
373 None => last_quote.ask_price,
374 };
375
376 let bid_size = match msg.bid_size {
377 Some(size) => parse_contracts_quantity(size, instrument),
378 None => last_quote.bid_size,
379 };
380
381 let ask_size = match msg.ask_size {
382 Some(size) => parse_contracts_quantity(size, instrument),
383 None => last_quote.ask_size,
384 };
385
386 let ts_event = UnixNanos::from(msg.timestamp);
387
388 QuoteTick::new(
389 instrument_id,
390 bid_price,
391 ask_price,
392 bid_size,
393 ask_size,
394 ts_event,
395 ts_init,
396 )
397}
398
399#[must_use]
401pub fn parse_trade_msg(
402 msg: &BitmexTradeMsg,
403 instrument: &InstrumentAny,
404 instrument_id: InstrumentId,
405 price_precision: u8,
406 ts_init: UnixNanos,
407) -> TradeTick {
408 let price = Price::new(msg.price, price_precision);
409 let size = parse_contracts_quantity(msg.size, instrument);
410 let aggressor_side = msg.side.as_aggressor_side();
411 let ts_event = UnixNanos::from(msg.timestamp);
412 let trade_id = match msg.trd_match_id {
413 Some(uuid) => TradeId::new(uuid.to_string()),
414 None => derive_trade_id(
415 msg.symbol,
416 ts_event.as_u64(),
417 msg.price,
418 msg.size as i64,
419 Some(msg.side.into()),
420 ),
421 };
422
423 TradeTick::new(
424 instrument_id,
425 price,
426 size,
427 aggressor_side,
428 trade_id,
429 ts_event,
430 ts_init,
431 )
432}
433
434#[must_use]
436pub fn parse_trade_bin_msg(
437 msg: &BitmexTradeBinMsg,
438 topic: &BitmexWsTopic,
439 instrument: &InstrumentAny,
440 instrument_id: InstrumentId,
441 price_precision: u8,
442 ts_init: UnixNanos,
443) -> Bar {
444 let spec = bar_spec_from_topic(topic);
445 let bar_type = BarType::new(instrument_id, spec, AggregationSource::External);
446
447 let open = Price::new(msg.open, price_precision);
448 let high = Price::new(msg.high, price_precision);
449 let low = Price::new(msg.low, price_precision);
450 let close = Price::new(msg.close, price_precision);
451
452 let (open, high, low, close) =
453 normalize_trade_bin_prices(open, high, low, close, &msg.symbol, Some(&bar_type));
454
455 let volume_contracts = normalize_trade_bin_volume(Some(msg.volume), &msg.symbol);
456 let volume = parse_contracts_quantity(volume_contracts, instrument);
457 let ts_event = UnixNanos::from(msg.timestamp);
458
459 Bar::new(bar_type, open, high, low, close, volume, ts_event, ts_init)
460}
461
462#[must_use]
466pub fn bar_spec_from_topic(topic: &BitmexWsTopic) -> BarSpecification {
467 match topic {
468 BitmexWsTopic::TradeBin1m => BAR_SPEC_1_MINUTE,
469 BitmexWsTopic::TradeBin5m => BAR_SPEC_5_MINUTE,
470 BitmexWsTopic::TradeBin1h => BAR_SPEC_1_HOUR,
471 BitmexWsTopic::TradeBin1d => BAR_SPEC_1_DAY,
472 _ => {
473 log::error!("Bar specification not supported: topic={topic:?}");
474 BAR_SPEC_1_MINUTE
475 }
476 }
477}
478
479#[must_use]
483pub fn topic_from_bar_spec(spec: BarSpecification) -> BitmexWsTopic {
484 match spec {
485 BAR_SPEC_1_MINUTE => BitmexWsTopic::TradeBin1m,
486 BAR_SPEC_5_MINUTE => BitmexWsTopic::TradeBin5m,
487 BAR_SPEC_1_HOUR => BitmexWsTopic::TradeBin1h,
488 BAR_SPEC_1_DAY => BitmexWsTopic::TradeBin1d,
489 _ => {
490 log::error!("Bar specification not supported: spec={spec:?}");
491 BitmexWsTopic::TradeBin1m
492 }
493 }
494}
495
496fn infer_order_type_from_msg(msg: &BitmexOrderMsg) -> OrderType {
497 if msg.stop_px.is_some() {
498 if msg.price.is_some() {
499 OrderType::StopLimit
500 } else {
501 OrderType::StopMarket
502 }
503 } else if msg.price.is_some() {
504 OrderType::Limit
505 } else {
506 OrderType::Market
507 }
508}
509
510pub fn parse_order_msg(
520 msg: &BitmexOrderMsg,
521 instrument: &InstrumentAny,
522 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
523 ts_init: UnixNanos,
524) -> anyhow::Result<OrderStatusReport> {
525 let account_id = bitmex_account_id(msg.account);
526 let instrument_id = parse_instrument_id(msg.symbol);
527 let venue_order_id = VenueOrderId::new(msg.order_id.to_string());
528 let common_side: BitmexSide = msg.side.into();
529 let order_side = OrderSide::from(common_side);
530
531 let order_type: OrderType = if let Some(ord_type) = msg.ord_type {
532 if ord_type == BitmexOrderType::Pegged
534 && msg.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
535 {
536 if msg.price.is_some() {
537 OrderType::TrailingStopLimit
538 } else {
539 OrderType::TrailingStopMarket
540 }
541 } else {
542 ord_type.into()
543 }
544 } else if let Some(client_order_id) = msg.cl_ord_id {
545 let client_order_id = ClientOrderId::new(client_order_id);
546 if let Some(&cached) = order_type_cache.get(&client_order_id) {
547 cached
548 } else {
549 let inferred = infer_order_type_from_msg(msg);
550 order_type_cache.insert(client_order_id, inferred);
551 inferred
552 }
553 } else {
554 infer_order_type_from_msg(msg)
555 };
556
557 let time_in_force: TimeInForce = match msg.time_in_force {
558 Some(tif) => tif.try_into().map_err(|e| anyhow::anyhow!("{e}"))?,
559 None => TimeInForce::Gtc,
560 };
561 let order_status: OrderStatus = msg.ord_status.into();
562 let quantity = parse_signed_contracts_quantity(msg.order_qty, instrument);
563 let filled_qty = parse_signed_contracts_quantity(msg.cum_qty, instrument);
564 let report_id = UUID4::new();
565 let ts_accepted =
566 parse_optional_datetime_to_unix_nanos(&Some(msg.transact_time), "transact_time");
567 let ts_last = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "timestamp");
568
569 let mut report = OrderStatusReport::new(
570 account_id,
571 instrument_id,
572 None, venue_order_id,
574 order_side.into(),
575 order_type,
576 time_in_force,
577 order_status,
578 quantity,
579 filled_qty,
580 ts_accepted,
581 ts_last,
582 ts_init,
583 Some(report_id),
584 );
585
586 if let Some(cl_ord_id) = &msg.cl_ord_id {
587 report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
588 }
589
590 if let Some(cl_ord_link_id) = &msg.cl_ord_link_id {
591 report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
592 }
593
594 if let Some(price) = msg.price {
595 report = report.with_price(Price::new(price, instrument.price_precision()));
596 }
597
598 if let Some(avg_px) = msg.avg_px {
599 report = report.with_avg_px(avg_px);
600 }
601
602 if let Some(trigger_price) = msg.stop_px {
603 report = report
604 .with_trigger_price(Price::new(trigger_price, instrument.price_precision()))
605 .with_trigger_type(extract_trigger_type(msg.exec_inst.as_ref()));
606 }
607
608 if matches!(
610 order_type,
611 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
612 ) && let Some(peg_offset) = msg.peg_offset_value
613 {
614 let trailing_offset = Decimal::try_from(peg_offset.abs())
615 .unwrap_or_else(|_| Decimal::new(peg_offset.abs() as i64, 0));
616 report = report
617 .with_trailing_offset(trailing_offset)
618 .with_trailing_offset_type(TrailingOffsetType::Price);
619
620 if msg.stop_px.is_none() {
621 report = report.with_trigger_type(extract_trigger_type(msg.exec_inst.as_ref()));
622 }
623 }
624
625 if let Some(exec_insts) = &msg.exec_inst {
626 for exec_inst in exec_insts {
627 match exec_inst {
628 BitmexExecInstruction::ParticipateDoNotInitiate => {
629 report = report.with_post_only(true);
630 }
631 BitmexExecInstruction::ReduceOnly => {
632 report = report.with_reduce_only(true);
633 }
634 _ => {}
635 }
636 }
637 }
638
639 if order_status == OrderStatus::Rejected {
641 if let Some(reason_str) = msg.ord_rej_reason.or(msg.text) {
642 log::debug!(
643 "Order rejected with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
644 venue_order_id,
645 msg.cl_ord_id,
646 reason_str,
647 );
648 report = report.with_cancel_reason(clean_reason(reason_str.as_ref()));
649 } else {
650 log::debug!(
651 "Order rejected without reason from BitMEX: order_id={:?}, client_order_id={:?}, ord_status={:?}, ord_rej_reason={:?}, text={:?}",
652 venue_order_id,
653 msg.cl_ord_id,
654 msg.ord_status,
655 msg.ord_rej_reason,
656 msg.text,
657 );
658 }
659 }
660
661 if order_status == OrderStatus::Canceled
664 && let Some(reason_str) = msg.ord_rej_reason.or(msg.text)
665 {
666 report = report.with_cancel_reason(clean_reason(reason_str.as_ref()));
667 }
668
669 Ok(report)
670}
671
672#[derive(Debug, Clone)]
674pub enum ParsedOrderEvent {
675 Accepted(OrderAccepted),
676 Canceled(OrderCanceled),
677 Expired(OrderExpired),
678 Triggered(OrderTriggered),
679 Rejected(OrderRejected),
680}
681
682pub fn parse_order_event(
688 msg: &BitmexOrderMsg,
689 client_order_id: ClientOrderId,
690 account_id: AccountId,
691 trader_id: TraderId,
692 strategy_id: StrategyId,
693 ts_init: UnixNanos,
694) -> Option<ParsedOrderEvent> {
695 let instrument_id = parse_instrument_id(msg.symbol);
696 let venue_order_id = VenueOrderId::new(msg.order_id.to_string());
697 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "timestamp");
698
699 match msg.ord_status {
700 BitmexOrderStatus::New => {
701 let accepted = OrderAccepted::new(
702 trader_id,
703 strategy_id,
704 instrument_id,
705 client_order_id,
706 venue_order_id,
707 account_id,
708 UUID4::new(),
709 ts_event,
710 ts_init,
711 false,
712 );
713 Some(ParsedOrderEvent::Accepted(accepted))
714 }
715 BitmexOrderStatus::Canceled => {
716 let cancel_reason = msg
719 .ord_rej_reason
720 .or(msg.text)
721 .map(|r| clean_reason(r.as_ref()));
722
723 let is_post_only_rejection = cancel_reason
724 .as_deref()
725 .is_some_and(|r| r.contains("ParticipateDoNotInitiate"));
726
727 if is_post_only_rejection {
728 let rejected = OrderRejected::new(
729 trader_id,
730 strategy_id,
731 instrument_id,
732 client_order_id,
733 account_id,
734 Ustr::from(
735 cancel_reason
736 .as_deref()
737 .unwrap_or("Post-only order rejected"),
738 ),
739 UUID4::new(),
740 ts_event,
741 ts_init,
742 false,
743 true, );
745 Some(ParsedOrderEvent::Rejected(rejected))
746 } else {
747 let canceled = OrderCanceled::new(
748 trader_id,
749 strategy_id,
750 instrument_id,
751 client_order_id,
752 UUID4::new(),
753 ts_event,
754 ts_init,
755 false,
756 Some(venue_order_id),
757 Some(account_id),
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(
1221 price_precision: u8,
1222 size_precision: u8,
1223 ) -> InstrumentAny {
1224 InstrumentAny::CryptoPerpetual(
1225 CryptoPerpetual::builder()
1226 .instrument_id(InstrumentId::from("XBTUSD.BITMEX"))
1227 .raw_symbol(Symbol::new("XBTUSD"))
1228 .base_currency(Currency::BTC())
1229 .quote_currency(Currency::USD())
1230 .settlement_currency(Currency::BTC())
1231 .is_inverse(true)
1232 .price_precision(price_precision)
1233 .size_precision(size_precision)
1234 .price_increment(Price::new(0.5, price_precision))
1235 .size_increment(Quantity::new(1.0, size_precision))
1236 .ts_event(UnixNanos::default())
1237 .ts_init(UnixNanos::default())
1238 .build()
1239 .unwrap(),
1240 )
1241 }
1242
1243 fn create_test_perpetual_instrument() -> InstrumentAny {
1244 create_test_perpetual_instrument_with_precisions(1, 0)
1245 }
1246
1247 #[rstest]
1248 fn test_orderbook_l2_message() {
1249 let json_data = load_test_json("ws_orderbook_l2.json");
1250
1251 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1252 let msg: BitmexOrderBookMsg = serde_json::from_str(&json_data).unwrap();
1253
1254 let instrument = create_test_perpetual_instrument();
1256
1257 let delta = parse_book_msg(
1259 &msg,
1260 &BitmexAction::Insert,
1261 &instrument,
1262 instrument.id(),
1263 instrument.price_precision(),
1264 UnixNanos::from(3),
1265 );
1266 assert_eq!(delta.instrument_id, instrument_id);
1267 assert_eq!(delta.order.price, Price::from("98459.9"));
1268 assert_eq!(delta.order.size, Quantity::from(33000));
1269 assert_eq!(delta.order.side, OrderSide::Sell.into());
1270 assert_eq!(delta.order.order_id, 62400580205);
1271 assert_eq!(delta.action, BookAction::Add);
1272 assert_eq!(delta.flags, 0);
1273 assert_eq!(delta.sequence, 0);
1274 assert_eq!(delta.ts_event, 1732436782356000000); assert_eq!(delta.ts_init, 3);
1276
1277 let delta = parse_book_msg(
1279 &msg,
1280 &BitmexAction::Partial,
1281 &instrument,
1282 instrument.id(),
1283 instrument.price_precision(),
1284 UnixNanos::from(3),
1285 );
1286 assert_eq!(delta.flags, RecordFlag::F_SNAPSHOT as u8);
1287 assert_eq!(delta.action, BookAction::Add);
1288
1289 let delta = parse_book_msg(
1291 &msg,
1292 &BitmexAction::Update,
1293 &instrument,
1294 instrument.id(),
1295 instrument.price_precision(),
1296 UnixNanos::from(3),
1297 );
1298 assert_eq!(delta.flags, 0);
1299 assert_eq!(delta.action, BookAction::Update);
1300 }
1301
1302 #[rstest]
1303 fn test_orderbook10_message() {
1304 let json_data = load_test_json("ws_orderbook_10.json");
1305 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1306 let msg: BitmexOrderBook10Msg = serde_json::from_str(&json_data).unwrap();
1307 let instrument = create_test_perpetual_instrument();
1308 let depth10 = parse_book10_msg(
1309 &msg,
1310 &instrument,
1311 instrument.id(),
1312 instrument.price_precision(),
1313 UnixNanos::from(3),
1314 )
1315 .unwrap();
1316
1317 assert_eq!(depth10.instrument_id, instrument_id);
1318
1319 assert_eq!(depth10.bids[0].price, Price::from("98490.3"));
1321 assert_eq!(depth10.bids[0].size, Quantity::from(22400));
1322 assert_eq!(depth10.bids[0].side, OrderSide::Buy.into());
1323
1324 assert_eq!(depth10.asks[0].price, Price::from("98490.4"));
1326 assert_eq!(depth10.asks[0].size, Quantity::from(17600));
1327 assert_eq!(depth10.asks[0].side, OrderSide::Sell.into());
1328
1329 assert_eq!(depth10.bid_counts, [1; DEPTH10_LEN]);
1331 assert_eq!(depth10.ask_counts, [1; DEPTH10_LEN]);
1332
1333 assert_eq!(depth10.sequence, 0);
1335 assert_eq!(depth10.flags, RecordFlag::F_SNAPSHOT as u8);
1336 assert_eq!(depth10.ts_event, 1732436353513000000); assert_eq!(depth10.ts_init, 3);
1338 }
1339
1340 #[rstest]
1341 fn test_quote_message() {
1342 let json_data = load_test_json("ws_quote.json");
1343
1344 let instrument_id = InstrumentId::from("BCHUSDT.BITMEX");
1345 let last_quote = QuoteTick::new(
1346 instrument_id,
1347 Price::new(487.50, 2),
1348 Price::new(488.20, 2),
1349 Quantity::from(100_000),
1350 Quantity::from(100_000),
1351 UnixNanos::from(1),
1352 UnixNanos::from(2),
1353 );
1354 let msg: BitmexQuoteMsg = serde_json::from_str(&json_data).unwrap();
1355 let instrument = create_test_perpetual_instrument_with_precisions(2, 0);
1356 let quote = parse_quote_msg(
1357 &msg,
1358 &last_quote,
1359 &instrument,
1360 instrument_id,
1361 instrument.price_precision(),
1362 UnixNanos::from(3),
1363 );
1364
1365 assert_eq!(quote.instrument_id, instrument_id);
1366 assert_eq!(quote.bid_price, Price::from("487.55"));
1367 assert_eq!(quote.ask_price, Price::from("488.25"));
1368 assert_eq!(quote.bid_size, Quantity::from(103_000));
1369 assert_eq!(quote.ask_size, Quantity::from(50_000));
1370 assert_eq!(quote.ts_event, 1732315465085000000);
1371 assert_eq!(quote.ts_init, 3);
1372 }
1373
1374 #[rstest]
1375 fn test_trade_message() {
1376 let json_data = load_test_json("ws_trade.json");
1377
1378 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1379 let msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1380 let instrument = create_test_perpetual_instrument();
1381 let trade = parse_trade_msg(
1382 &msg,
1383 &instrument,
1384 instrument.id(),
1385 instrument.price_precision(),
1386 UnixNanos::from(3),
1387 );
1388
1389 assert_eq!(trade.instrument_id, instrument_id);
1390 assert_eq!(trade.price, Price::from("98570.9"));
1391 assert_eq!(trade.size, Quantity::from(100));
1392 assert_eq!(trade.aggressor_side, AggressorSide::Sell);
1393 assert_eq!(
1394 trade.trade_id.to_string(),
1395 "00000000-006d-1000-0000-000e8737d536"
1396 );
1397 assert_eq!(trade.ts_event, 1732436138704000000); assert_eq!(trade.ts_init, 3);
1399 }
1400
1401 #[rstest]
1402 fn test_trade_message_derives_trade_id_when_trd_match_id_missing() {
1403 let json_data = load_test_json("ws_trade.json");
1404 let mut msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1405 msg.trd_match_id = None;
1406 let instrument = create_test_perpetual_instrument();
1407
1408 let trade = parse_trade_msg(
1409 &msg,
1410 &instrument,
1411 instrument.id(),
1412 instrument.price_precision(),
1413 UnixNanos::from(3),
1414 );
1415
1416 let mut again_msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1417 again_msg.trd_match_id = None;
1418 let again = parse_trade_msg(
1419 &again_msg,
1420 &instrument,
1421 instrument.id(),
1422 instrument.price_precision(),
1423 UnixNanos::from(3),
1424 );
1425
1426 assert_eq!(trade.trade_id, again.trade_id, "derivation must be stable");
1427 assert_eq!(trade.trade_id.as_str().len(), 16);
1428
1429 let mut altered: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1430 altered.trd_match_id = None;
1431 altered.price += 1.0;
1432 let altered_trade = parse_trade_msg(
1433 &altered,
1434 &instrument,
1435 instrument.id(),
1436 instrument.price_precision(),
1437 UnixNanos::from(3),
1438 );
1439 assert_ne!(trade.trade_id, altered_trade.trade_id);
1440 }
1441
1442 #[rstest]
1443 fn test_trade_bin_message() {
1444 let json_data = load_test_json("ws_trade_bin_1m.json");
1445
1446 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1447 let topic = BitmexWsTopic::TradeBin1m;
1448
1449 let msg: BitmexTradeBinMsg = serde_json::from_str(&json_data).unwrap();
1450 let instrument = create_test_perpetual_instrument();
1451 let bar = parse_trade_bin_msg(
1452 &msg,
1453 &topic,
1454 &instrument,
1455 instrument.id(),
1456 instrument.price_precision(),
1457 UnixNanos::from(3),
1458 );
1459
1460 assert_eq!(bar.instrument_id(), instrument_id);
1461 assert_eq!(
1462 bar.bar_type.spec(),
1463 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1464 );
1465 assert_eq!(bar.open, Price::from("97550.0"));
1466 assert_eq!(bar.high, Price::from("97584.4"));
1467 assert_eq!(bar.low, Price::from("97550.0"));
1468 assert_eq!(bar.close, Price::from("97570.1"));
1469 assert_eq!(bar.volume, Quantity::from(84_000));
1470 assert_eq!(bar.ts_event, 1732392420000000000); assert_eq!(bar.ts_init, 3);
1472 }
1473
1474 #[rstest]
1475 fn test_trade_bin_message_extreme_adjustment() {
1476 let topic = BitmexWsTopic::TradeBin1m;
1477 let instrument = create_test_perpetual_instrument();
1478
1479 let msg = BitmexTradeBinMsg {
1480 timestamp: "2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap(),
1481 symbol: Ustr::from("XBTUSD"),
1482 open: 50_000.0,
1483 high: 49_990.0,
1484 low: 50_010.0,
1485 close: 50_005.0,
1486 trades: 10,
1487 volume: 1_000,
1488 vwap: Some(0.0),
1489 last_size: Some(0),
1490 turnover: 0,
1491 home_notional: 0.0,
1492 foreign_notional: 0.0,
1493 pool: None,
1494 };
1495
1496 let bar = parse_trade_bin_msg(
1497 &msg,
1498 &topic,
1499 &instrument,
1500 instrument.id(),
1501 instrument.price_precision(),
1502 UnixNanos::from(3),
1503 );
1504
1505 assert_eq!(bar.high, Price::from("50010.0"));
1506 assert_eq!(bar.low, Price::from("49990.0"));
1507 assert_eq!(bar.open, Price::from("50000.0"));
1508 assert_eq!(bar.close, Price::from("50005.0"));
1509 assert_eq!(bar.volume, Quantity::from(1_000));
1510 }
1511
1512 #[rstest]
1513 fn test_parse_order_msg() {
1514 let json_data = load_test_json("ws_order.json");
1515 let mut msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1516 msg.avg_px = Some(Decimal::from_str("30000.500000000004").unwrap());
1517 let mut cache = AHashMap::new();
1518 let instrument = create_test_perpetual_instrument();
1519 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1520
1521 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1522 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1523 assert_eq!(
1524 report.venue_order_id.to_string(),
1525 "550e8400-e29b-41d4-a716-446655440001"
1526 );
1527 assert_eq!(
1528 report.client_order_id.unwrap().to_string(),
1529 "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA"
1530 );
1531 assert_eq!(report.order_side, OrderSide::Buy.into());
1532 assert_eq!(report.order_type, OrderType::Limit);
1533 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1534 assert_eq!(report.order_status, OrderStatus::Accepted);
1535 assert_eq!(report.quantity, Quantity::from(100));
1536 assert_eq!(report.filled_qty, Quantity::from(0));
1537 assert_eq!(report.price.unwrap(), Price::from("98000.0"));
1538 assert_eq!(
1539 report.avg_px,
1540 Some(Decimal::from_str("30000.500000000004").unwrap())
1541 );
1542 assert_eq!(report.ts_accepted, 1732530600000000000); }
1544
1545 #[rstest]
1546 fn test_parse_order_msg_infers_type_when_missing() {
1547 let json_data = load_test_json("ws_order.json");
1548 let mut msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1549 msg.ord_type = None;
1550 msg.cl_ord_id = None;
1551 msg.price = Some(98_000.0);
1552 msg.stop_px = None;
1553
1554 let mut cache = AHashMap::new();
1555 let instrument = create_test_perpetual_instrument();
1556
1557 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1558
1559 assert_eq!(report.order_type, OrderType::Limit);
1560 }
1561
1562 #[rstest]
1563 fn test_parse_order_msg_rejected_with_reason() {
1564 let mut msg: BitmexOrderMsg =
1565 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1566 msg.ord_status = BitmexOrderStatus::Rejected;
1567 msg.ord_rej_reason = Some(Ustr::from("Insufficient available balance"));
1568 msg.text = None;
1569 msg.cum_qty = 0;
1570
1571 let mut cache = AHashMap::new();
1572 let instrument = create_test_perpetual_instrument();
1573 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1574
1575 assert_eq!(report.order_status, OrderStatus::Rejected);
1576 assert_eq!(
1577 report.cancel_reason,
1578 Some("Insufficient available balance".to_string())
1579 );
1580 }
1581
1582 #[rstest]
1583 fn test_parse_order_msg_rejected_with_text_fallback() {
1584 let mut msg: BitmexOrderMsg =
1585 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1586 msg.ord_status = BitmexOrderStatus::Rejected;
1587 msg.ord_rej_reason = None;
1588 msg.text = Some(Ustr::from("Order would execute immediately"));
1589 msg.cum_qty = 0;
1590
1591 let mut cache = AHashMap::new();
1592 let instrument = create_test_perpetual_instrument();
1593 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1594
1595 assert_eq!(report.order_status, OrderStatus::Rejected);
1596 assert_eq!(
1597 report.cancel_reason,
1598 Some("Order would execute immediately".to_string())
1599 );
1600 }
1601
1602 #[rstest]
1603 fn test_parse_order_msg_rejected_without_reason() {
1604 let mut msg: BitmexOrderMsg =
1605 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1606 msg.ord_status = BitmexOrderStatus::Rejected;
1607 msg.ord_rej_reason = None;
1608 msg.text = None;
1609 msg.cum_qty = 0;
1610
1611 let mut cache = AHashMap::new();
1612 let instrument = create_test_perpetual_instrument();
1613 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1614
1615 assert_eq!(report.order_status, OrderStatus::Rejected);
1616 assert_eq!(report.cancel_reason, None);
1617 }
1618
1619 #[rstest]
1620 fn test_parse_execution_msg() {
1621 let json_data = load_test_json("ws_execution.json");
1622 let msg: BitmexExecutionMsg = serde_json::from_str(&json_data).unwrap();
1623 let instrument = create_test_perpetual_instrument();
1624 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1625
1626 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1627 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1628 assert_eq!(
1629 fill.venue_order_id.to_string(),
1630 "550e8400-e29b-41d4-a716-446655440002"
1631 );
1632 assert_eq!(
1633 fill.trade_id.to_string(),
1634 "00000000-006d-1000-0000-000e8737d540"
1635 );
1636 assert_eq!(
1637 fill.client_order_id.unwrap().to_string(),
1638 "mm_bitmex_2b/oemUeQ4CAJZgP3fjHsB"
1639 );
1640 assert_eq!(fill.order_side, OrderSide::Sell);
1641 assert_eq!(fill.last_qty, Quantity::from(100));
1642 assert_eq!(fill.last_px, Price::from("98950.0"));
1643 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
1644 assert_eq!(fill.commission, Money::new(0.00075, Currency::from("XBT")));
1645 assert_eq!(fill.commission.currency.code.to_string(), "XBT");
1646 assert_eq!(fill.ts_event, 1732530900789000000); }
1648
1649 #[rstest]
1650 fn test_parse_execution_msg_non_trade() {
1651 let mut msg: BitmexExecutionMsg =
1653 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1654 msg.exec_type = Some(BitmexExecType::Settlement);
1655
1656 let instrument = create_test_perpetual_instrument();
1657 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1658 assert!(result.is_none());
1659 }
1660
1661 #[rstest]
1662 fn test_parse_cancel_reject_execution() {
1663 let json = load_test_json("ws_execution_cancel_reject.json");
1665
1666 let msg: BitmexExecutionMsg = serde_json::from_str(&json).unwrap();
1667 assert_eq!(msg.exec_type, Some(BitmexExecType::CancelReject));
1668 assert_eq!(msg.ord_status, Some(BitmexOrderStatus::Rejected));
1669 assert_eq!(msg.symbol, None);
1670
1671 let instrument = create_test_perpetual_instrument();
1673 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1674 assert!(result.is_none());
1675 }
1676
1677 #[rstest]
1678 fn test_parse_execution_msg_liquidation() {
1679 let mut msg: BitmexExecutionMsg =
1681 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1682 msg.exec_type = Some(BitmexExecType::Liquidation);
1683
1684 let instrument = create_test_perpetual_instrument();
1685 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1686
1687 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1688 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1689 assert_eq!(fill.order_side, OrderSide::Sell);
1690 assert_eq!(fill.last_qty, Quantity::from(100));
1691 assert_eq!(fill.last_px, Price::from("98950.0"));
1692 }
1693
1694 #[rstest]
1695 fn test_parse_execution_msg_bankruptcy() {
1696 let mut msg: BitmexExecutionMsg =
1697 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1698 msg.exec_type = Some(BitmexExecType::Bankruptcy);
1699
1700 let instrument = create_test_perpetual_instrument();
1701 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1702
1703 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1704 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1705 assert_eq!(fill.order_side, OrderSide::Sell);
1706 assert_eq!(fill.last_qty, Quantity::from(100));
1707 }
1708
1709 #[rstest]
1710 fn test_parse_execution_msg_settlement() {
1711 let mut msg: BitmexExecutionMsg =
1712 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1713 msg.exec_type = Some(BitmexExecType::Settlement);
1714
1715 let instrument = create_test_perpetual_instrument();
1716 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1717 assert!(result.is_none());
1718 }
1719
1720 #[rstest]
1721 fn test_parse_execution_msg_trial_fill() {
1722 let mut msg: BitmexExecutionMsg =
1723 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1724 msg.exec_type = Some(BitmexExecType::TrialFill);
1725
1726 let instrument = create_test_perpetual_instrument();
1727 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1728 assert!(result.is_none());
1729 }
1730
1731 #[rstest]
1732 fn test_parse_execution_msg_funding() {
1733 let mut msg: BitmexExecutionMsg =
1734 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1735 msg.exec_type = Some(BitmexExecType::Funding);
1736
1737 let instrument = create_test_perpetual_instrument();
1738 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1739 assert!(result.is_none());
1740 }
1741
1742 #[rstest]
1743 fn test_parse_execution_msg_insurance() {
1744 let mut msg: BitmexExecutionMsg =
1745 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1746 msg.exec_type = Some(BitmexExecType::Insurance);
1747
1748 let instrument = create_test_perpetual_instrument();
1749 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1750 assert!(result.is_none());
1751 }
1752
1753 #[rstest]
1754 fn test_parse_execution_msg_rebalance() {
1755 let mut msg: BitmexExecutionMsg =
1756 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1757 msg.exec_type = Some(BitmexExecType::Rebalance);
1758
1759 let instrument = create_test_perpetual_instrument();
1760 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1761 assert!(result.is_none());
1762 }
1763
1764 #[rstest]
1765 fn test_parse_execution_msg_order_state_changes() {
1766 let instrument = create_test_perpetual_instrument();
1767
1768 let order_state_types = vec![
1769 BitmexExecType::New,
1770 BitmexExecType::Canceled,
1771 BitmexExecType::CancelReject,
1772 BitmexExecType::Replaced,
1773 BitmexExecType::Rejected,
1774 BitmexExecType::AmendReject,
1775 BitmexExecType::Suspended,
1776 BitmexExecType::Released,
1777 BitmexExecType::TriggeredOrActivatedBySystem,
1778 ];
1779
1780 for exec_type in order_state_types {
1781 let mut msg: BitmexExecutionMsg =
1782 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1783 msg.exec_type = Some(exec_type.clone());
1784
1785 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1786 assert!(
1787 result.is_none(),
1788 "Expected None for exec_type {exec_type:?}"
1789 );
1790 }
1791 }
1792
1793 #[rstest]
1794 fn test_parse_position_msg() {
1795 let json_data = load_test_json("ws_position.json");
1796 let msg: BitmexPositionMsg = serde_json::from_str(&json_data).unwrap();
1797 let instrument = create_test_perpetual_instrument();
1798 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1799
1800 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1801 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1802 assert_eq!(report.position_side, PositionSide::Long);
1803 assert_eq!(report.quantity, Quantity::from(1000));
1804 assert!(report.venue_position_id.is_none());
1805 assert_eq!(report.ts_last, 1732530900789000000); }
1807
1808 #[rstest]
1809 fn test_parse_position_msg_short() {
1810 let mut msg: BitmexPositionMsg =
1811 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1812 msg.current_qty = Some(-500);
1813
1814 let instrument = create_test_perpetual_instrument();
1815 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1816 assert_eq!(report.position_side, PositionSide::Short);
1817 assert_eq!(report.quantity, Quantity::from(500));
1818 }
1819
1820 #[rstest]
1821 fn test_parse_position_msg_flat() {
1822 let mut msg: BitmexPositionMsg =
1823 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1824 msg.current_qty = Some(0);
1825
1826 let instrument = create_test_perpetual_instrument();
1827 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1828 assert_eq!(report.position_side, PositionSide::Flat);
1829 assert_eq!(report.quantity, Quantity::from(0));
1830 }
1831
1832 #[rstest]
1833 fn test_parse_wallet_msg() {
1834 let json_data = load_test_json("ws_wallet.json");
1835 let msg: BitmexWalletMsg = serde_json::from_str(&json_data).unwrap();
1836 let ts_init = UnixNanos::from(1);
1837 let account_state = parse_wallet_msg(&msg, ts_init);
1838
1839 assert_eq!(account_state.account_id.to_string(), "BITMEX-1234567");
1840 assert!(!account_state.balances.is_empty());
1841 let balance = &account_state.balances[0];
1842 assert_eq!(balance.currency.code.to_string(), "XBT");
1843 assert!((balance.total.as_f64() - 1.0000518).abs() < 1e-7);
1845 assert_eq!(balance.locked.as_f64(), 0.0);
1847 assert_eq!(balance.free.as_decimal(), balance.total.as_decimal());
1848 }
1849
1850 #[rstest]
1851 fn test_parse_wallet_msg_no_amount() {
1852 let mut msg: BitmexWalletMsg =
1853 serde_json::from_str(&load_test_json("ws_wallet.json")).unwrap();
1854 msg.amount = None;
1855
1856 let ts_init = UnixNanos::from(1);
1857 let account_state = parse_wallet_msg(&msg, ts_init);
1858 let balance = &account_state.balances[0];
1859 assert_eq!(balance.total.as_f64(), 0.0);
1860 }
1861
1862 #[rstest]
1863 fn test_parse_margin_msg() {
1864 let json_data = load_test_json("ws_margin.json");
1865 let msg: BitmexMarginMsg = serde_json::from_str(&json_data).unwrap();
1866 let margin_balance = parse_margin_msg(&msg);
1867
1868 assert_eq!(margin_balance.currency.code.to_string(), "XBT");
1869 assert!(margin_balance.instrument_id.is_none());
1870 assert_eq!(margin_balance.initial.as_f64(), 0.0);
1873 assert!((margin_balance.maintenance.as_f64() - 0.00015949).abs() < 1e-8);
1875 }
1876
1877 #[rstest]
1878 fn test_parse_margin_msg_no_available() {
1879 let mut msg: BitmexMarginMsg =
1880 serde_json::from_str(&load_test_json("ws_margin.json")).unwrap();
1881 msg.available_margin = None;
1882
1883 let margin_balance = parse_margin_msg(&msg);
1884 assert!(margin_balance.initial.as_f64() >= 0.0);
1886 assert!(margin_balance.maintenance.as_f64() >= 0.0);
1887 }
1888
1889 #[rstest]
1890 fn test_parse_margin_account_state_includes_margins() {
1891 let msg = BitmexMarginMsg {
1892 account: 123456,
1893 currency: Ustr::from("USDt"),
1894 risk_limit: None,
1895 amount: Some(5_000_000_000),
1896 prev_realised_pnl: None,
1897 gross_comm: None,
1898 gross_open_cost: None,
1899 gross_open_premium: None,
1900 gross_exec_cost: None,
1901 gross_mark_value: None,
1902 risk_value: None,
1903 init_margin: Some(200_000_000), maint_margin: Some(100_000_000), target_excess_margin: None,
1906 realised_pnl: None,
1907 unrealised_pnl: None,
1908 wallet_balance: Some(5_000_000_000), margin_balance: None,
1910 margin_leverage: None,
1911 margin_used_pcnt: None,
1912 excess_margin: None,
1913 available_margin: Some(4_800_000_000), withdrawable_margin: None,
1915 maker_fee_discount: None,
1916 taker_fee_discount: None,
1917 timestamp: Timestamp::from_second(1_700_000_000).unwrap(),
1918 foreign_margin_balance: None,
1919 foreign_requirement: None,
1920 };
1921
1922 let ts_init = UnixNanos::from(1_000_000_000u64);
1923 let state = parse_margin_account_state(&msg, ts_init);
1924
1925 assert_eq!(state.account_id.to_string(), "BITMEX-123456");
1926 assert_eq!(state.account_type, AccountType::Margin);
1927 assert_eq!(state.balances.len(), 1);
1928 assert_eq!(state.margins.len(), 1);
1929
1930 let balance = &state.balances[0];
1931 assert_eq!(balance.total.as_f64(), 5000.0);
1932
1933 let margin = &state.margins[0];
1934 assert!(margin.instrument_id.is_none());
1935 assert_eq!(margin.currency.code.as_str(), "USDT");
1936 assert_eq!(margin.initial.as_f64(), 200.0);
1937 assert_eq!(margin.maintenance.as_f64(), 100.0);
1938 }
1939
1940 #[rstest]
1941 fn test_parse_margin_account_state_zero_margins_excluded() {
1942 let msg = BitmexMarginMsg {
1943 account: 123456,
1944 currency: Ustr::from("XBt"),
1945 risk_limit: None,
1946 amount: Some(100_000_000),
1947 prev_realised_pnl: None,
1948 gross_comm: None,
1949 gross_open_cost: None,
1950 gross_open_premium: None,
1951 gross_exec_cost: None,
1952 gross_mark_value: None,
1953 risk_value: None,
1954 init_margin: Some(0),
1955 maint_margin: Some(0),
1956 target_excess_margin: None,
1957 realised_pnl: None,
1958 unrealised_pnl: None,
1959 wallet_balance: Some(100_000_000),
1960 margin_balance: None,
1961 margin_leverage: None,
1962 margin_used_pcnt: None,
1963 excess_margin: None,
1964 available_margin: Some(100_000_000),
1965 withdrawable_margin: None,
1966 maker_fee_discount: None,
1967 taker_fee_discount: None,
1968 timestamp: Timestamp::from_second(1_700_000_000).unwrap(),
1969 foreign_margin_balance: None,
1970 foreign_requirement: None,
1971 };
1972
1973 let state = parse_margin_account_state(&msg, UnixNanos::from(1_000_000_000u64));
1974
1975 assert_eq!(state.balances.len(), 1);
1976 assert_eq!(state.margins.len(), 0);
1977 }
1978
1979 #[rstest]
1980 fn test_parse_instrument_msg_both_prices() {
1981 let json_data = load_test_json("ws_instrument.json");
1982 let msg: BitmexInstrumentMsg = serde_json::from_str(&json_data).unwrap();
1983
1984 let mut instruments_cache = AHashMap::new();
1986 let test_instrument = create_test_perpetual_instrument();
1987 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
1988
1989 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
1990
1991 assert_eq!(updates.len(), 2);
1993
1994 match &updates[0] {
1995 Data::MarkPrice(update) => {
1996 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
1997 assert_eq!(update.value.as_f64(), 95125.7);
1998 }
1999 _ => panic!("Expected MarkPriceUpdate at index 0"),
2000 }
2001
2002 match &updates[1] {
2003 Data::IndexPrice(update) => {
2004 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2005 assert_eq!(update.value.as_f64(), 95126.0);
2006 }
2007 _ => panic!("Expected IndexPriceUpdate at index 1"),
2008 }
2009 }
2010
2011 #[rstest]
2012 fn test_parse_instrument_msg_mark_price_only() {
2013 let mut msg: BitmexInstrumentMsg =
2014 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2015 msg.index_price = None;
2016 msg.indicative_settle_price = None;
2017
2018 let mut instruments_cache = AHashMap::new();
2019 let test_instrument = create_test_perpetual_instrument();
2020 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2021
2022 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2023
2024 assert_eq!(updates.len(), 1);
2025 match &updates[0] {
2026 Data::MarkPrice(update) => {
2027 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2028 assert_eq!(update.value.as_f64(), 95125.7);
2029 }
2030 _ => panic!("Expected MarkPriceUpdate"),
2031 }
2032 }
2033
2034 #[rstest]
2035 fn test_parse_instrument_msg_index_price_only() {
2036 let mut msg: BitmexInstrumentMsg =
2037 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2038 msg.mark_price = None;
2039 msg.fair_price = None;
2040
2041 let mut instruments_cache = AHashMap::new();
2042 let test_instrument = create_test_perpetual_instrument();
2043 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2044
2045 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2046
2047 assert_eq!(updates.len(), 1);
2048 match &updates[0] {
2049 Data::IndexPrice(update) => {
2050 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2051 assert_eq!(update.value.as_f64(), 95126.0);
2052 }
2053 _ => panic!("Expected IndexPriceUpdate"),
2054 }
2055 }
2056
2057 #[rstest]
2058 fn test_parse_instrument_msg_no_prices() {
2059 let mut msg: BitmexInstrumentMsg =
2060 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2061 msg.mark_price = None;
2062 msg.fair_price = None;
2063 msg.index_price = None;
2064 msg.indicative_settle_price = None;
2065 msg.last_price = None;
2066
2067 let mut instruments_cache = AHashMap::new();
2069 let test_instrument = create_test_perpetual_instrument();
2070 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2071
2072 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2073 assert_eq!(updates.len(), 0);
2074 }
2075
2076 #[rstest]
2077 fn test_parse_instrument_msg_index_symbol() {
2078 let mut msg: BitmexInstrumentMsg =
2081 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2082 msg.symbol = Ustr::from(".BXBT");
2083 msg.last_price = Some(119163.05);
2084 msg.mark_price = Some(119163.05); msg.fair_price = None;
2086 msg.index_price = None;
2087 msg.indicative_settle_price = None;
2088
2089 let instrument_id = InstrumentId::from(".BXBT.BITMEX");
2091 let instrument = CryptoPerpetual::builder()
2092 .instrument_id(instrument_id)
2093 .raw_symbol(Symbol::from(".BXBT"))
2094 .base_currency(Currency::BTC())
2095 .quote_currency(Currency::USD())
2096 .settlement_currency(Currency::USD())
2097 .is_inverse(false)
2098 .price_precision(2)
2100 .size_precision(8)
2101 .price_increment(Price::from("0.01"))
2102 .size_increment(Quantity::from("0.00000001"))
2103 .ts_event(UnixNanos::default())
2104 .ts_init(UnixNanos::default())
2105 .build()
2106 .unwrap();
2107 let mut instruments_cache = AHashMap::new();
2108 instruments_cache.insert(
2109 Ustr::from(".BXBT"),
2110 InstrumentAny::CryptoPerpetual(instrument),
2111 );
2112
2113 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2114
2115 assert_eq!(updates.len(), 2);
2116
2117 match &updates[0] {
2119 Data::MarkPrice(update) => {
2120 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2121 assert_eq!(update.value, Price::from("119163.05"));
2122 }
2123 _ => panic!("Expected MarkPriceUpdate for index symbol"),
2124 }
2125
2126 match &updates[1] {
2128 Data::IndexPrice(update) => {
2129 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2130 assert_eq!(update.value, Price::from("119163.05"));
2131 assert_eq!(update.ts_init, UnixNanos::from(1));
2132 }
2133 _ => panic!("Expected IndexPriceUpdate for index symbol"),
2134 }
2135 }
2136
2137 #[rstest]
2139 fn test_parse_instrument_msg_mark_update_wire_shape() {
2140 let msg: BitmexInstrumentMsg =
2141 serde_json::from_str(&load_test_json("ws_instrument_mark_update.json")).unwrap();
2142
2143 let instrument_id = InstrumentId::from("DOTUSDT.BITMEX");
2144 let instrument = CryptoPerpetual::builder()
2145 .instrument_id(instrument_id)
2146 .raw_symbol(Symbol::from("DOTUSDT"))
2147 .base_currency(Currency::from_str("DOT").unwrap())
2148 .quote_currency(Currency::USDT())
2149 .settlement_currency(Currency::USDT())
2150 .is_inverse(false)
2151 .price_precision(4)
2153 .size_precision(8)
2154 .price_increment(Price::from("0.0001"))
2155 .size_increment(Quantity::from("0.00000001"))
2156 .ts_event(UnixNanos::default())
2157 .ts_init(UnixNanos::default())
2158 .build()
2159 .unwrap();
2160 let mut instruments_cache = AHashMap::new();
2161 instruments_cache.insert(
2162 Ustr::from("DOTUSDT"),
2163 InstrumentAny::CryptoPerpetual(instrument),
2164 );
2165
2166 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2167
2168 assert_eq!(updates.len(), 1);
2169 match &updates[0] {
2170 Data::MarkPrice(update) => {
2171 assert_eq!(update.instrument_id.to_string(), "DOTUSDT.BITMEX");
2172 assert_eq!(update.value, Price::from("1.2669"));
2173 }
2174 _ => panic!("Expected single MarkPriceUpdate for mark-update wire shape"),
2175 }
2176 }
2177
2178 #[rstest]
2180 fn test_parse_instrument_msg_index_update_wire_shape() {
2181 let msg: BitmexInstrumentMsg =
2182 serde_json::from_str(&load_test_json("ws_instrument_index_update.json")).unwrap();
2183
2184 let mut instruments_cache = AHashMap::new();
2185 instruments_cache.insert(
2186 Ustr::from("XBTUSD"),
2187 create_test_perpetual_instrument_with_precisions(2, 0),
2188 );
2189
2190 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2191
2192 assert_eq!(updates.len(), 1);
2193 match &updates[0] {
2194 Data::IndexPrice(update) => {
2195 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2196 assert_eq!(update.value, Price::from("75847.62"));
2197 }
2198 _ => panic!("Expected single IndexPriceUpdate for index-update wire shape"),
2199 }
2200 }
2201
2202 #[rstest]
2203 fn test_parse_funding_msg() {
2204 let json_data = load_test_json("ws_funding_rate.json");
2205 let msg: BitmexFundingMsg = serde_json::from_str(&json_data).unwrap();
2206 let update = parse_funding_msg(&msg, UnixNanos::from(1));
2207
2208 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2209 assert_eq!(update.rate.to_string(), "0.0001");
2210 assert_eq!(update.interval, Some(60 * 8));
2211 assert!(update.next_funding_ns.is_none());
2212 assert_eq!(update.ts_event, UnixNanos::from(1732507200000000000));
2213 assert_eq!(update.ts_init, UnixNanos::from(1));
2214 }
2215}