1use std::{num::NonZero, str::FromStr};
19
20use ahash::AHashMap;
21use chrono::Timelike;
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 = common_side.into();
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,
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 .map(|p| Price::new(p, instrument.price_precision()));
812
813 let trigger_price = None;
815 let protection_price = None;
817
818 let event_id = UUID4::new();
819 let ts_event = parse_optional_datetime_to_unix_nanos(&msg.timestamp, "timestamp");
820
821 Some(OrderUpdated::new(
822 trader_id,
823 strategy_id,
824 instrument_id,
825 client_order_id,
826 quantity,
827 event_id,
828 ts_event,
829 ts_init,
830 false, venue_order_id,
832 Some(account_id),
833 price,
834 trigger_price,
835 protection_price,
836 false, ))
838}
839
840pub fn parse_execution_msg(
854 msg: BitmexExecutionMsg,
855 instrument: &InstrumentAny,
856 ts_init: UnixNanos,
857) -> Option<FillReport> {
858 let exec_type = msg.exec_type?;
859
860 match exec_type {
861 BitmexExecType::Trade | BitmexExecType::Liquidation => {}
863 BitmexExecType::Bankruptcy => {
864 log::warn!(
865 "Processing bankruptcy execution as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
866 msg.order_id,
867 msg.symbol,
868 );
869 }
870
871 BitmexExecType::Settlement => {
873 log::debug!(
874 "Settlement execution skipped (not a fill): applies quanto conversion/PnL transfer on contract settlement: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
875 msg.order_id,
876 msg.symbol,
877 );
878 return None;
879 }
880 BitmexExecType::TrialFill => {
881 log::warn!(
882 "Trial fill execution received (testnet only), not processed as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
883 msg.order_id,
884 msg.symbol,
885 );
886 return None;
887 }
888
889 BitmexExecType::Funding => {
891 log::debug!(
892 "Funding execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
893 msg.order_id,
894 msg.symbol,
895 );
896 return None;
897 }
898 BitmexExecType::Insurance => {
899 log::debug!(
900 "Insurance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
901 msg.order_id,
902 msg.symbol,
903 );
904 return None;
905 }
906 BitmexExecType::Rebalance => {
907 log::debug!(
908 "Rebalance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
909 msg.order_id,
910 msg.symbol,
911 );
912 return None;
913 }
914
915 BitmexExecType::New
917 | BitmexExecType::Canceled
918 | BitmexExecType::CancelReject
919 | BitmexExecType::Replaced
920 | BitmexExecType::Rejected
921 | BitmexExecType::AmendReject
922 | BitmexExecType::Suspended
923 | BitmexExecType::Released
924 | BitmexExecType::TriggeredOrActivatedBySystem => {
925 log::debug!(
926 "Execution message skipped (order state change, not a fill): exec_type={exec_type:?}, order_id={:?}",
927 msg.order_id,
928 );
929 return None;
930 }
931
932 BitmexExecType::Unknown(ref type_str) => {
933 log::warn!(
934 "Unknown execution type received, skipping: exec_type={type_str}, order_id={:?}, symbol={:?}",
935 msg.order_id,
936 msg.symbol,
937 );
938 return None;
939 }
940 }
941
942 let account_id = bitmex_account_id(msg.account?);
943 let instrument_id = parse_instrument_id(msg.symbol?);
944 let venue_order_id = VenueOrderId::new(msg.order_id?.to_string());
945 let trade_id = TradeId::new(msg.trd_match_id?.to_string());
946 let order_side: OrderSide = msg.side.map_or(OrderSide::NoOrderSide, |s| {
947 let side: BitmexSide = s.into();
948 side.into()
949 });
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).as_specified();
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(&Ustr::from(&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::MarkPriceUpdate(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::IndexPriceUpdate(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 interval_hours = msg.funding_interval.hour();
1104 let interval_minutes = msg.funding_interval.minute();
1105 let interval = Some((interval_hours * 60 + interval_minutes) as u16);
1106 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "");
1107
1108 FundingRateUpdate::new(
1109 instrument_id,
1110 msg.funding_rate,
1111 interval,
1112 None, ts_event,
1114 ts_init,
1115 )
1116}
1117
1118#[must_use]
1127pub fn parse_wallet_msg(msg: &BitmexWalletMsg, ts_init: UnixNanos) -> AccountState {
1128 let account_id = bitmex_account_id(msg.account);
1129
1130 let currency_str = map_bitmex_currency(msg.currency.as_str());
1132 let currency = get_currency(¤cy_str);
1133
1134 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1137 let amount_dec = Decimal::from(msg.amount.unwrap_or(0)) / divisor;
1138
1139 let balance = AccountBalance::from_total_and_locked(amount_dec, Decimal::ZERO, currency)
1140 .expect("Balance calculation should be valid");
1141
1142 AccountState::new(
1143 account_id,
1144 AccountType::Margin,
1145 vec![balance],
1146 vec![], true, UUID4::new(),
1149 ts_init,
1150 ts_init,
1151 None,
1152 )
1153}
1154
1155#[must_use]
1157pub fn parse_margin_msg(msg: &BitmexMarginMsg) -> MarginBalance {
1158 let currency_str = map_bitmex_currency(msg.currency.as_str());
1159 let currency = get_currency(¤cy_str);
1160
1161 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1162 let initial_dec = Decimal::from(msg.init_margin.unwrap_or(0).max(0)) / divisor;
1163 let maintenance_dec = Decimal::from(msg.maint_margin.unwrap_or(0).max(0)) / divisor;
1164
1165 MarginBalance::new(
1166 Money::from_decimal(initial_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1167 Money::from_decimal(maintenance_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1168 None,
1169 )
1170}
1171
1172#[must_use]
1174pub fn parse_margin_account_state(msg: &BitmexMarginMsg, ts_init: UnixNanos) -> AccountState {
1175 let account_id = bitmex_account_id(msg.account);
1176 let balance = parse_account_balance(msg);
1177
1178 let margin = parse_margin_msg(msg);
1179
1180 let margins = if !margin.initial.is_zero() || !margin.maintenance.is_zero() {
1181 vec![margin]
1182 } else {
1183 vec![]
1184 };
1185
1186 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "margin.timestamp");
1187
1188 AccountState::new(
1189 account_id,
1190 AccountType::Margin,
1191 vec![balance],
1192 margins,
1193 true,
1194 UUID4::new(),
1195 ts_event,
1196 ts_init,
1197 None,
1198 )
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use chrono::{DateTime, Utc};
1204 use nautilus_model::{
1205 enums::{AggressorSide, BookAction, LiquiditySide, PositionSide},
1206 identifiers::Symbol,
1207 instruments::crypto_perpetual::CryptoPerpetual,
1208 };
1209 use rstest::rstest;
1210 use ustr::Ustr;
1211
1212 use super::*;
1213 use crate::common::{
1214 enums::{BitmexExecType, BitmexOrderStatus},
1215 testing::load_test_json,
1216 };
1217
1218 fn create_test_perpetual_instrument_with_precisions(
1220 price_precision: u8,
1221 size_precision: u8,
1222 ) -> InstrumentAny {
1223 InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
1224 InstrumentId::from("XBTUSD.BITMEX"),
1225 Symbol::new("XBTUSD"),
1226 Currency::BTC(),
1227 Currency::USD(),
1228 Currency::BTC(),
1229 true, price_precision,
1231 size_precision,
1232 Price::new(0.5, price_precision),
1233 Quantity::new(1.0, size_precision),
1234 None, None, None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::default(),
1249 UnixNanos::default(),
1250 ))
1251 }
1252
1253 fn create_test_perpetual_instrument() -> InstrumentAny {
1254 create_test_perpetual_instrument_with_precisions(1, 0)
1255 }
1256
1257 #[rstest]
1258 fn test_orderbook_l2_message() {
1259 let json_data = load_test_json("ws_orderbook_l2.json");
1260
1261 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1262 let msg: BitmexOrderBookMsg = serde_json::from_str(&json_data).unwrap();
1263
1264 let instrument = create_test_perpetual_instrument();
1266
1267 let delta = parse_book_msg(
1269 &msg,
1270 &BitmexAction::Insert,
1271 &instrument,
1272 instrument.id(),
1273 instrument.price_precision(),
1274 UnixNanos::from(3),
1275 );
1276 assert_eq!(delta.instrument_id, instrument_id);
1277 assert_eq!(delta.order.price, Price::from("98459.9"));
1278 assert_eq!(delta.order.size, Quantity::from(33000));
1279 assert_eq!(delta.order.side, OrderSide::Sell);
1280 assert_eq!(delta.order.order_id, 62400580205);
1281 assert_eq!(delta.action, BookAction::Add);
1282 assert_eq!(delta.flags, 0);
1283 assert_eq!(delta.sequence, 0);
1284 assert_eq!(delta.ts_event, 1732436782356000000); assert_eq!(delta.ts_init, 3);
1286
1287 let delta = parse_book_msg(
1289 &msg,
1290 &BitmexAction::Partial,
1291 &instrument,
1292 instrument.id(),
1293 instrument.price_precision(),
1294 UnixNanos::from(3),
1295 );
1296 assert_eq!(delta.flags, RecordFlag::F_SNAPSHOT as u8);
1297 assert_eq!(delta.action, BookAction::Add);
1298
1299 let delta = parse_book_msg(
1301 &msg,
1302 &BitmexAction::Update,
1303 &instrument,
1304 instrument.id(),
1305 instrument.price_precision(),
1306 UnixNanos::from(3),
1307 );
1308 assert_eq!(delta.flags, 0);
1309 assert_eq!(delta.action, BookAction::Update);
1310 }
1311
1312 #[rstest]
1313 fn test_orderbook10_message() {
1314 let json_data = load_test_json("ws_orderbook_10.json");
1315 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1316 let msg: BitmexOrderBook10Msg = serde_json::from_str(&json_data).unwrap();
1317 let instrument = create_test_perpetual_instrument();
1318 let depth10 = parse_book10_msg(
1319 &msg,
1320 &instrument,
1321 instrument.id(),
1322 instrument.price_precision(),
1323 UnixNanos::from(3),
1324 )
1325 .unwrap();
1326
1327 assert_eq!(depth10.instrument_id, instrument_id);
1328
1329 assert_eq!(depth10.bids[0].price, Price::from("98490.3"));
1331 assert_eq!(depth10.bids[0].size, Quantity::from(22400));
1332 assert_eq!(depth10.bids[0].side, OrderSide::Buy);
1333
1334 assert_eq!(depth10.asks[0].price, Price::from("98490.4"));
1336 assert_eq!(depth10.asks[0].size, Quantity::from(17600));
1337 assert_eq!(depth10.asks[0].side, OrderSide::Sell);
1338
1339 assert_eq!(depth10.bid_counts, [1; DEPTH10_LEN]);
1341 assert_eq!(depth10.ask_counts, [1; DEPTH10_LEN]);
1342
1343 assert_eq!(depth10.sequence, 0);
1345 assert_eq!(depth10.flags, RecordFlag::F_SNAPSHOT as u8);
1346 assert_eq!(depth10.ts_event, 1732436353513000000); assert_eq!(depth10.ts_init, 3);
1348 }
1349
1350 #[rstest]
1351 fn test_quote_message() {
1352 let json_data = load_test_json("ws_quote.json");
1353
1354 let instrument_id = InstrumentId::from("BCHUSDT.BITMEX");
1355 let last_quote = QuoteTick::new(
1356 instrument_id,
1357 Price::new(487.50, 2),
1358 Price::new(488.20, 2),
1359 Quantity::from(100_000),
1360 Quantity::from(100_000),
1361 UnixNanos::from(1),
1362 UnixNanos::from(2),
1363 );
1364 let msg: BitmexQuoteMsg = serde_json::from_str(&json_data).unwrap();
1365 let instrument = create_test_perpetual_instrument_with_precisions(2, 0);
1366 let quote = parse_quote_msg(
1367 &msg,
1368 &last_quote,
1369 &instrument,
1370 instrument_id,
1371 instrument.price_precision(),
1372 UnixNanos::from(3),
1373 );
1374
1375 assert_eq!(quote.instrument_id, instrument_id);
1376 assert_eq!(quote.bid_price, Price::from("487.55"));
1377 assert_eq!(quote.ask_price, Price::from("488.25"));
1378 assert_eq!(quote.bid_size, Quantity::from(103_000));
1379 assert_eq!(quote.ask_size, Quantity::from(50_000));
1380 assert_eq!(quote.ts_event, 1732315465085000000);
1381 assert_eq!(quote.ts_init, 3);
1382 }
1383
1384 #[rstest]
1385 fn test_trade_message() {
1386 let json_data = load_test_json("ws_trade.json");
1387
1388 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1389 let msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1390 let instrument = create_test_perpetual_instrument();
1391 let trade = parse_trade_msg(
1392 &msg,
1393 &instrument,
1394 instrument.id(),
1395 instrument.price_precision(),
1396 UnixNanos::from(3),
1397 );
1398
1399 assert_eq!(trade.instrument_id, instrument_id);
1400 assert_eq!(trade.price, Price::from("98570.9"));
1401 assert_eq!(trade.size, Quantity::from(100));
1402 assert_eq!(trade.aggressor_side, AggressorSide::Seller);
1403 assert_eq!(
1404 trade.trade_id.to_string(),
1405 "00000000-006d-1000-0000-000e8737d536"
1406 );
1407 assert_eq!(trade.ts_event, 1732436138704000000); assert_eq!(trade.ts_init, 3);
1409 }
1410
1411 #[rstest]
1412 fn test_trade_message_derives_trade_id_when_trd_match_id_missing() {
1413 let json_data = load_test_json("ws_trade.json");
1414 let mut msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1415 msg.trd_match_id = None;
1416 let instrument = create_test_perpetual_instrument();
1417
1418 let trade = parse_trade_msg(
1419 &msg,
1420 &instrument,
1421 instrument.id(),
1422 instrument.price_precision(),
1423 UnixNanos::from(3),
1424 );
1425
1426 let mut again_msg: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1427 again_msg.trd_match_id = None;
1428 let again = parse_trade_msg(
1429 &again_msg,
1430 &instrument,
1431 instrument.id(),
1432 instrument.price_precision(),
1433 UnixNanos::from(3),
1434 );
1435
1436 assert_eq!(trade.trade_id, again.trade_id, "derivation must be stable");
1437 assert_eq!(trade.trade_id.as_str().len(), 16);
1438
1439 let mut altered: BitmexTradeMsg = serde_json::from_str(&json_data).unwrap();
1440 altered.trd_match_id = None;
1441 altered.price += 1.0;
1442 let altered_trade = parse_trade_msg(
1443 &altered,
1444 &instrument,
1445 instrument.id(),
1446 instrument.price_precision(),
1447 UnixNanos::from(3),
1448 );
1449 assert_ne!(trade.trade_id, altered_trade.trade_id);
1450 }
1451
1452 #[rstest]
1453 fn test_trade_bin_message() {
1454 let json_data = load_test_json("ws_trade_bin_1m.json");
1455
1456 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1457 let topic = BitmexWsTopic::TradeBin1m;
1458
1459 let msg: BitmexTradeBinMsg = serde_json::from_str(&json_data).unwrap();
1460 let instrument = create_test_perpetual_instrument();
1461 let bar = parse_trade_bin_msg(
1462 &msg,
1463 &topic,
1464 &instrument,
1465 instrument.id(),
1466 instrument.price_precision(),
1467 UnixNanos::from(3),
1468 );
1469
1470 assert_eq!(bar.instrument_id(), instrument_id);
1471 assert_eq!(
1472 bar.bar_type.spec(),
1473 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1474 );
1475 assert_eq!(bar.open, Price::from("97550.0"));
1476 assert_eq!(bar.high, Price::from("97584.4"));
1477 assert_eq!(bar.low, Price::from("97550.0"));
1478 assert_eq!(bar.close, Price::from("97570.1"));
1479 assert_eq!(bar.volume, Quantity::from(84_000));
1480 assert_eq!(bar.ts_event, 1732392420000000000); assert_eq!(bar.ts_init, 3);
1482 }
1483
1484 #[rstest]
1485 fn test_trade_bin_message_extreme_adjustment() {
1486 let topic = BitmexWsTopic::TradeBin1m;
1487 let instrument = create_test_perpetual_instrument();
1488
1489 let msg = BitmexTradeBinMsg {
1490 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1491 .unwrap()
1492 .with_timezone(&Utc),
1493 symbol: Ustr::from("XBTUSD"),
1494 open: 50_000.0,
1495 high: 49_990.0,
1496 low: 50_010.0,
1497 close: 50_005.0,
1498 trades: 10,
1499 volume: 1_000,
1500 vwap: Some(0.0),
1501 last_size: Some(0),
1502 turnover: 0,
1503 home_notional: 0.0,
1504 foreign_notional: 0.0,
1505 pool: None,
1506 };
1507
1508 let bar = parse_trade_bin_msg(
1509 &msg,
1510 &topic,
1511 &instrument,
1512 instrument.id(),
1513 instrument.price_precision(),
1514 UnixNanos::from(3),
1515 );
1516
1517 assert_eq!(bar.high, Price::from("50010.0"));
1518 assert_eq!(bar.low, Price::from("49990.0"));
1519 assert_eq!(bar.open, Price::from("50000.0"));
1520 assert_eq!(bar.close, Price::from("50005.0"));
1521 assert_eq!(bar.volume, Quantity::from(1_000));
1522 }
1523
1524 #[rstest]
1525 fn test_parse_order_msg() {
1526 let json_data = load_test_json("ws_order.json");
1527 let msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1528 let mut cache = AHashMap::new();
1529 let instrument = create_test_perpetual_instrument();
1530 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1531
1532 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1533 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1534 assert_eq!(
1535 report.venue_order_id.to_string(),
1536 "550e8400-e29b-41d4-a716-446655440001"
1537 );
1538 assert_eq!(
1539 report.client_order_id.unwrap().to_string(),
1540 "mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA"
1541 );
1542 assert_eq!(report.order_side, OrderSide::Buy);
1543 assert_eq!(report.order_type, OrderType::Limit);
1544 assert_eq!(report.time_in_force, TimeInForce::Gtc);
1545 assert_eq!(report.order_status, OrderStatus::Accepted);
1546 assert_eq!(report.quantity, Quantity::from(100));
1547 assert_eq!(report.filled_qty, Quantity::from(0));
1548 assert_eq!(report.price.unwrap(), Price::from("98000.0"));
1549 assert_eq!(report.ts_accepted, 1732530600000000000); }
1551
1552 #[rstest]
1553 fn test_parse_order_msg_infers_type_when_missing() {
1554 let json_data = load_test_json("ws_order.json");
1555 let mut msg: BitmexOrderMsg = serde_json::from_str(&json_data).unwrap();
1556 msg.ord_type = None;
1557 msg.cl_ord_id = None;
1558 msg.price = Some(98_000.0);
1559 msg.stop_px = None;
1560
1561 let mut cache = AHashMap::new();
1562 let instrument = create_test_perpetual_instrument();
1563
1564 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1565
1566 assert_eq!(report.order_type, OrderType::Limit);
1567 }
1568
1569 #[rstest]
1570 fn test_parse_order_msg_rejected_with_reason() {
1571 let mut msg: BitmexOrderMsg =
1572 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1573 msg.ord_status = BitmexOrderStatus::Rejected;
1574 msg.ord_rej_reason = Some(Ustr::from("Insufficient available balance"));
1575 msg.text = None;
1576 msg.cum_qty = 0;
1577
1578 let mut cache = AHashMap::new();
1579 let instrument = create_test_perpetual_instrument();
1580 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1581
1582 assert_eq!(report.order_status, OrderStatus::Rejected);
1583 assert_eq!(
1584 report.cancel_reason,
1585 Some("Insufficient available balance".to_string())
1586 );
1587 }
1588
1589 #[rstest]
1590 fn test_parse_order_msg_rejected_with_text_fallback() {
1591 let mut msg: BitmexOrderMsg =
1592 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1593 msg.ord_status = BitmexOrderStatus::Rejected;
1594 msg.ord_rej_reason = None;
1595 msg.text = Some(Ustr::from("Order would execute immediately"));
1596 msg.cum_qty = 0;
1597
1598 let mut cache = AHashMap::new();
1599 let instrument = create_test_perpetual_instrument();
1600 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1601
1602 assert_eq!(report.order_status, OrderStatus::Rejected);
1603 assert_eq!(
1604 report.cancel_reason,
1605 Some("Order would execute immediately".to_string())
1606 );
1607 }
1608
1609 #[rstest]
1610 fn test_parse_order_msg_rejected_without_reason() {
1611 let mut msg: BitmexOrderMsg =
1612 serde_json::from_str(&load_test_json("ws_order.json")).unwrap();
1613 msg.ord_status = BitmexOrderStatus::Rejected;
1614 msg.ord_rej_reason = None;
1615 msg.text = None;
1616 msg.cum_qty = 0;
1617
1618 let mut cache = AHashMap::new();
1619 let instrument = create_test_perpetual_instrument();
1620 let report = parse_order_msg(&msg, &instrument, &mut cache, UnixNanos::default()).unwrap();
1621
1622 assert_eq!(report.order_status, OrderStatus::Rejected);
1623 assert_eq!(report.cancel_reason, None);
1624 }
1625
1626 #[rstest]
1627 fn test_parse_execution_msg() {
1628 let json_data = load_test_json("ws_execution.json");
1629 let msg: BitmexExecutionMsg = serde_json::from_str(&json_data).unwrap();
1630 let instrument = create_test_perpetual_instrument();
1631 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1632
1633 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1634 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1635 assert_eq!(
1636 fill.venue_order_id.to_string(),
1637 "550e8400-e29b-41d4-a716-446655440002"
1638 );
1639 assert_eq!(
1640 fill.trade_id.to_string(),
1641 "00000000-006d-1000-0000-000e8737d540"
1642 );
1643 assert_eq!(
1644 fill.client_order_id.unwrap().to_string(),
1645 "mm_bitmex_2b/oemUeQ4CAJZgP3fjHsB"
1646 );
1647 assert_eq!(fill.order_side, OrderSide::Sell);
1648 assert_eq!(fill.last_qty, Quantity::from(100));
1649 assert_eq!(fill.last_px, Price::from("98950.0"));
1650 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
1651 assert_eq!(fill.commission, Money::new(0.00075, Currency::from("XBT")));
1652 assert_eq!(fill.commission.currency.code.to_string(), "XBT");
1653 assert_eq!(fill.ts_event, 1732530900789000000); }
1655
1656 #[rstest]
1657 fn test_parse_execution_msg_non_trade() {
1658 let mut msg: BitmexExecutionMsg =
1660 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1661 msg.exec_type = Some(BitmexExecType::Settlement);
1662
1663 let instrument = create_test_perpetual_instrument();
1664 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1665 assert!(result.is_none());
1666 }
1667
1668 #[rstest]
1669 fn test_parse_cancel_reject_execution() {
1670 let json = load_test_json("ws_execution_cancel_reject.json");
1672
1673 let msg: BitmexExecutionMsg = serde_json::from_str(&json).unwrap();
1674 assert_eq!(msg.exec_type, Some(BitmexExecType::CancelReject));
1675 assert_eq!(msg.ord_status, Some(BitmexOrderStatus::Rejected));
1676 assert_eq!(msg.symbol, None);
1677
1678 let instrument = create_test_perpetual_instrument();
1680 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1681 assert!(result.is_none());
1682 }
1683
1684 #[rstest]
1685 fn test_parse_execution_msg_liquidation() {
1686 let mut msg: BitmexExecutionMsg =
1688 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1689 msg.exec_type = Some(BitmexExecType::Liquidation);
1690
1691 let instrument = create_test_perpetual_instrument();
1692 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1693
1694 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1695 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1696 assert_eq!(fill.order_side, OrderSide::Sell);
1697 assert_eq!(fill.last_qty, Quantity::from(100));
1698 assert_eq!(fill.last_px, Price::from("98950.0"));
1699 }
1700
1701 #[rstest]
1702 fn test_parse_execution_msg_bankruptcy() {
1703 let mut msg: BitmexExecutionMsg =
1704 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1705 msg.exec_type = Some(BitmexExecType::Bankruptcy);
1706
1707 let instrument = create_test_perpetual_instrument();
1708 let fill = parse_execution_msg(msg, &instrument, UnixNanos::default()).unwrap();
1709
1710 assert_eq!(fill.account_id.to_string(), "BITMEX-1234567");
1711 assert_eq!(fill.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1712 assert_eq!(fill.order_side, OrderSide::Sell);
1713 assert_eq!(fill.last_qty, Quantity::from(100));
1714 }
1715
1716 #[rstest]
1717 fn test_parse_execution_msg_settlement() {
1718 let mut msg: BitmexExecutionMsg =
1719 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1720 msg.exec_type = Some(BitmexExecType::Settlement);
1721
1722 let instrument = create_test_perpetual_instrument();
1723 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1724 assert!(result.is_none());
1725 }
1726
1727 #[rstest]
1728 fn test_parse_execution_msg_trial_fill() {
1729 let mut msg: BitmexExecutionMsg =
1730 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1731 msg.exec_type = Some(BitmexExecType::TrialFill);
1732
1733 let instrument = create_test_perpetual_instrument();
1734 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1735 assert!(result.is_none());
1736 }
1737
1738 #[rstest]
1739 fn test_parse_execution_msg_funding() {
1740 let mut msg: BitmexExecutionMsg =
1741 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1742 msg.exec_type = Some(BitmexExecType::Funding);
1743
1744 let instrument = create_test_perpetual_instrument();
1745 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1746 assert!(result.is_none());
1747 }
1748
1749 #[rstest]
1750 fn test_parse_execution_msg_insurance() {
1751 let mut msg: BitmexExecutionMsg =
1752 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1753 msg.exec_type = Some(BitmexExecType::Insurance);
1754
1755 let instrument = create_test_perpetual_instrument();
1756 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1757 assert!(result.is_none());
1758 }
1759
1760 #[rstest]
1761 fn test_parse_execution_msg_rebalance() {
1762 let mut msg: BitmexExecutionMsg =
1763 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1764 msg.exec_type = Some(BitmexExecType::Rebalance);
1765
1766 let instrument = create_test_perpetual_instrument();
1767 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1768 assert!(result.is_none());
1769 }
1770
1771 #[rstest]
1772 fn test_parse_execution_msg_order_state_changes() {
1773 let instrument = create_test_perpetual_instrument();
1774
1775 let order_state_types = vec![
1776 BitmexExecType::New,
1777 BitmexExecType::Canceled,
1778 BitmexExecType::CancelReject,
1779 BitmexExecType::Replaced,
1780 BitmexExecType::Rejected,
1781 BitmexExecType::AmendReject,
1782 BitmexExecType::Suspended,
1783 BitmexExecType::Released,
1784 BitmexExecType::TriggeredOrActivatedBySystem,
1785 ];
1786
1787 for exec_type in order_state_types {
1788 let mut msg: BitmexExecutionMsg =
1789 serde_json::from_str(&load_test_json("ws_execution.json")).unwrap();
1790 msg.exec_type = Some(exec_type.clone());
1791
1792 let result = parse_execution_msg(msg, &instrument, UnixNanos::default());
1793 assert!(
1794 result.is_none(),
1795 "Expected None for exec_type {exec_type:?}"
1796 );
1797 }
1798 }
1799
1800 #[rstest]
1801 fn test_parse_position_msg() {
1802 let json_data = load_test_json("ws_position.json");
1803 let msg: BitmexPositionMsg = serde_json::from_str(&json_data).unwrap();
1804 let instrument = create_test_perpetual_instrument();
1805 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1806
1807 assert_eq!(report.account_id.to_string(), "BITMEX-1234567");
1808 assert_eq!(report.instrument_id, InstrumentId::from("XBTUSD.BITMEX"));
1809 assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
1810 assert_eq!(report.quantity, Quantity::from(1000));
1811 assert!(report.venue_position_id.is_none());
1812 assert_eq!(report.ts_last, 1732530900789000000); }
1814
1815 #[rstest]
1816 fn test_parse_position_msg_short() {
1817 let mut msg: BitmexPositionMsg =
1818 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1819 msg.current_qty = Some(-500);
1820
1821 let instrument = create_test_perpetual_instrument();
1822 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1823 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
1824 assert_eq!(report.quantity, Quantity::from(500));
1825 }
1826
1827 #[rstest]
1828 fn test_parse_position_msg_flat() {
1829 let mut msg: BitmexPositionMsg =
1830 serde_json::from_str(&load_test_json("ws_position.json")).unwrap();
1831 msg.current_qty = Some(0);
1832
1833 let instrument = create_test_perpetual_instrument();
1834 let report = parse_position_msg(&msg, &instrument, UnixNanos::default());
1835 assert_eq!(report.position_side.as_position_side(), PositionSide::Flat);
1836 assert_eq!(report.quantity, Quantity::from(0));
1837 }
1838
1839 #[rstest]
1840 fn test_parse_wallet_msg() {
1841 let json_data = load_test_json("ws_wallet.json");
1842 let msg: BitmexWalletMsg = serde_json::from_str(&json_data).unwrap();
1843 let ts_init = UnixNanos::from(1);
1844 let account_state = parse_wallet_msg(&msg, ts_init);
1845
1846 assert_eq!(account_state.account_id.to_string(), "BITMEX-1234567");
1847 assert!(!account_state.balances.is_empty());
1848 let balance = &account_state.balances[0];
1849 assert_eq!(balance.currency.code.to_string(), "XBT");
1850 assert!((balance.total.as_f64() - 1.0000518).abs() < 1e-7);
1852 assert_eq!(balance.locked.as_f64(), 0.0);
1854 assert_eq!(balance.free.as_decimal(), balance.total.as_decimal());
1855 }
1856
1857 #[rstest]
1858 fn test_parse_wallet_msg_no_amount() {
1859 let mut msg: BitmexWalletMsg =
1860 serde_json::from_str(&load_test_json("ws_wallet.json")).unwrap();
1861 msg.amount = None;
1862
1863 let ts_init = UnixNanos::from(1);
1864 let account_state = parse_wallet_msg(&msg, ts_init);
1865 let balance = &account_state.balances[0];
1866 assert_eq!(balance.total.as_f64(), 0.0);
1867 }
1868
1869 #[rstest]
1870 fn test_parse_margin_msg() {
1871 let json_data = load_test_json("ws_margin.json");
1872 let msg: BitmexMarginMsg = serde_json::from_str(&json_data).unwrap();
1873 let margin_balance = parse_margin_msg(&msg);
1874
1875 assert_eq!(margin_balance.currency.code.to_string(), "XBT");
1876 assert!(margin_balance.instrument_id.is_none());
1877 assert_eq!(margin_balance.initial.as_f64(), 0.0);
1880 assert!((margin_balance.maintenance.as_f64() - 0.00015949).abs() < 1e-8);
1882 }
1883
1884 #[rstest]
1885 fn test_parse_margin_msg_no_available() {
1886 let mut msg: BitmexMarginMsg =
1887 serde_json::from_str(&load_test_json("ws_margin.json")).unwrap();
1888 msg.available_margin = None;
1889
1890 let margin_balance = parse_margin_msg(&msg);
1891 assert!(margin_balance.initial.as_f64() >= 0.0);
1893 assert!(margin_balance.maintenance.as_f64() >= 0.0);
1894 }
1895
1896 #[rstest]
1897 fn test_parse_margin_account_state_includes_margins() {
1898 let msg = BitmexMarginMsg {
1899 account: 123456,
1900 currency: Ustr::from("USDt"),
1901 risk_limit: None,
1902 amount: Some(5_000_000_000),
1903 prev_realised_pnl: None,
1904 gross_comm: None,
1905 gross_open_cost: None,
1906 gross_open_premium: None,
1907 gross_exec_cost: None,
1908 gross_mark_value: None,
1909 risk_value: None,
1910 init_margin: Some(200_000_000), maint_margin: Some(100_000_000), target_excess_margin: None,
1913 realised_pnl: None,
1914 unrealised_pnl: None,
1915 wallet_balance: Some(5_000_000_000), margin_balance: None,
1917 margin_leverage: None,
1918 margin_used_pcnt: None,
1919 excess_margin: None,
1920 available_margin: Some(4_800_000_000), withdrawable_margin: None,
1922 maker_fee_discount: None,
1923 taker_fee_discount: None,
1924 timestamp: DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
1925 foreign_margin_balance: None,
1926 foreign_requirement: None,
1927 };
1928
1929 let ts_init = UnixNanos::from(1_000_000_000u64);
1930 let state = parse_margin_account_state(&msg, ts_init);
1931
1932 assert_eq!(state.account_id.to_string(), "BITMEX-123456");
1933 assert_eq!(state.account_type, AccountType::Margin);
1934 assert_eq!(state.balances.len(), 1);
1935 assert_eq!(state.margins.len(), 1);
1936
1937 let balance = &state.balances[0];
1938 assert_eq!(balance.total.as_f64(), 5000.0);
1939
1940 let margin = &state.margins[0];
1941 assert!(margin.instrument_id.is_none());
1942 assert_eq!(margin.currency.code.as_str(), "USDT");
1943 assert_eq!(margin.initial.as_f64(), 200.0);
1944 assert_eq!(margin.maintenance.as_f64(), 100.0);
1945 }
1946
1947 #[rstest]
1948 fn test_parse_margin_account_state_zero_margins_excluded() {
1949 let msg = BitmexMarginMsg {
1950 account: 123456,
1951 currency: Ustr::from("XBt"),
1952 risk_limit: None,
1953 amount: Some(100_000_000),
1954 prev_realised_pnl: None,
1955 gross_comm: None,
1956 gross_open_cost: None,
1957 gross_open_premium: None,
1958 gross_exec_cost: None,
1959 gross_mark_value: None,
1960 risk_value: None,
1961 init_margin: Some(0),
1962 maint_margin: Some(0),
1963 target_excess_margin: None,
1964 realised_pnl: None,
1965 unrealised_pnl: None,
1966 wallet_balance: Some(100_000_000),
1967 margin_balance: None,
1968 margin_leverage: None,
1969 margin_used_pcnt: None,
1970 excess_margin: None,
1971 available_margin: Some(100_000_000),
1972 withdrawable_margin: None,
1973 maker_fee_discount: None,
1974 taker_fee_discount: None,
1975 timestamp: DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
1976 foreign_margin_balance: None,
1977 foreign_requirement: None,
1978 };
1979
1980 let state = parse_margin_account_state(&msg, UnixNanos::from(1_000_000_000u64));
1981
1982 assert_eq!(state.balances.len(), 1);
1983 assert_eq!(state.margins.len(), 0);
1984 }
1985
1986 #[rstest]
1987 fn test_parse_instrument_msg_both_prices() {
1988 let json_data = load_test_json("ws_instrument.json");
1989 let msg: BitmexInstrumentMsg = serde_json::from_str(&json_data).unwrap();
1990
1991 let mut instruments_cache = AHashMap::new();
1993 let test_instrument = create_test_perpetual_instrument();
1994 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
1995
1996 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
1997
1998 assert_eq!(updates.len(), 2);
2000
2001 match &updates[0] {
2002 Data::MarkPriceUpdate(update) => {
2003 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2004 assert_eq!(update.value.as_f64(), 95125.7);
2005 }
2006 _ => panic!("Expected MarkPriceUpdate at index 0"),
2007 }
2008
2009 match &updates[1] {
2010 Data::IndexPriceUpdate(update) => {
2011 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2012 assert_eq!(update.value.as_f64(), 95126.0);
2013 }
2014 _ => panic!("Expected IndexPriceUpdate at index 1"),
2015 }
2016 }
2017
2018 #[rstest]
2019 fn test_parse_instrument_msg_mark_price_only() {
2020 let mut msg: BitmexInstrumentMsg =
2021 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2022 msg.index_price = None;
2023 msg.indicative_settle_price = None;
2024
2025 let mut instruments_cache = AHashMap::new();
2026 let test_instrument = create_test_perpetual_instrument();
2027 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2028
2029 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2030
2031 assert_eq!(updates.len(), 1);
2032 match &updates[0] {
2033 Data::MarkPriceUpdate(update) => {
2034 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2035 assert_eq!(update.value.as_f64(), 95125.7);
2036 }
2037 _ => panic!("Expected MarkPriceUpdate"),
2038 }
2039 }
2040
2041 #[rstest]
2042 fn test_parse_instrument_msg_index_price_only() {
2043 let mut msg: BitmexInstrumentMsg =
2044 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2045 msg.mark_price = None;
2046 msg.fair_price = None;
2047
2048 let mut instruments_cache = AHashMap::new();
2049 let test_instrument = create_test_perpetual_instrument();
2050 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2051
2052 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2053
2054 assert_eq!(updates.len(), 1);
2055 match &updates[0] {
2056 Data::IndexPriceUpdate(update) => {
2057 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2058 assert_eq!(update.value.as_f64(), 95126.0);
2059 }
2060 _ => panic!("Expected IndexPriceUpdate"),
2061 }
2062 }
2063
2064 #[rstest]
2065 fn test_parse_instrument_msg_no_prices() {
2066 let mut msg: BitmexInstrumentMsg =
2067 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2068 msg.mark_price = None;
2069 msg.fair_price = None;
2070 msg.index_price = None;
2071 msg.indicative_settle_price = None;
2072 msg.last_price = None;
2073
2074 let mut instruments_cache = AHashMap::new();
2076 let test_instrument = create_test_perpetual_instrument();
2077 instruments_cache.insert(Ustr::from("XBTUSD"), test_instrument);
2078
2079 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2080 assert_eq!(updates.len(), 0);
2081 }
2082
2083 #[rstest]
2084 fn test_parse_instrument_msg_index_symbol() {
2085 let mut msg: BitmexInstrumentMsg =
2088 serde_json::from_str(&load_test_json("ws_instrument.json")).unwrap();
2089 msg.symbol = Ustr::from(".BXBT");
2090 msg.last_price = Some(119163.05);
2091 msg.mark_price = Some(119163.05); msg.fair_price = None;
2093 msg.index_price = None;
2094 msg.indicative_settle_price = None;
2095
2096 let instrument_id = InstrumentId::from(".BXBT.BITMEX");
2098 let instrument = CryptoPerpetual::new(
2099 instrument_id,
2100 Symbol::from(".BXBT"),
2101 Currency::BTC(),
2102 Currency::USD(),
2103 Currency::USD(),
2104 false, 2, 8, Price::from("0.01"),
2108 Quantity::from("0.00000001"),
2109 None, None, None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::default(), UnixNanos::default(), );
2126 let mut instruments_cache = AHashMap::new();
2127 instruments_cache.insert(
2128 Ustr::from(".BXBT"),
2129 InstrumentAny::CryptoPerpetual(instrument),
2130 );
2131
2132 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2133
2134 assert_eq!(updates.len(), 2);
2135
2136 match &updates[0] {
2138 Data::MarkPriceUpdate(update) => {
2139 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2140 assert_eq!(update.value, Price::from("119163.05"));
2141 }
2142 _ => panic!("Expected MarkPriceUpdate for index symbol"),
2143 }
2144
2145 match &updates[1] {
2147 Data::IndexPriceUpdate(update) => {
2148 assert_eq!(update.instrument_id.to_string(), ".BXBT.BITMEX");
2149 assert_eq!(update.value, Price::from("119163.05"));
2150 assert_eq!(update.ts_init, UnixNanos::from(1));
2151 }
2152 _ => panic!("Expected IndexPriceUpdate for index symbol"),
2153 }
2154 }
2155
2156 #[rstest]
2158 fn test_parse_instrument_msg_mark_update_wire_shape() {
2159 let msg: BitmexInstrumentMsg =
2160 serde_json::from_str(&load_test_json("ws_instrument_mark_update.json")).unwrap();
2161
2162 let instrument_id = InstrumentId::from("DOTUSDT.BITMEX");
2163 let instrument = CryptoPerpetual::new(
2164 instrument_id,
2165 Symbol::from("DOTUSDT"),
2166 Currency::from_str("DOT").unwrap(),
2167 Currency::USDT(),
2168 Currency::USDT(),
2169 false, 4, 8, Price::from("0.0001"),
2173 Quantity::from("0.00000001"),
2174 None,
2175 None,
2176 None,
2177 None,
2178 None,
2179 None,
2180 None,
2181 None,
2182 None,
2183 None,
2184 None,
2185 None,
2186 None,
2187 None,
2188 UnixNanos::default(),
2189 UnixNanos::default(),
2190 );
2191 let mut instruments_cache = AHashMap::new();
2192 instruments_cache.insert(
2193 Ustr::from("DOTUSDT"),
2194 InstrumentAny::CryptoPerpetual(instrument),
2195 );
2196
2197 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2198
2199 assert_eq!(updates.len(), 1);
2200 match &updates[0] {
2201 Data::MarkPriceUpdate(update) => {
2202 assert_eq!(update.instrument_id.to_string(), "DOTUSDT.BITMEX");
2203 assert_eq!(update.value, Price::from("1.2669"));
2204 }
2205 _ => panic!("Expected single MarkPriceUpdate for mark-update wire shape"),
2206 }
2207 }
2208
2209 #[rstest]
2211 fn test_parse_instrument_msg_index_update_wire_shape() {
2212 let msg: BitmexInstrumentMsg =
2213 serde_json::from_str(&load_test_json("ws_instrument_index_update.json")).unwrap();
2214
2215 let mut instruments_cache = AHashMap::new();
2216 instruments_cache.insert(
2217 Ustr::from("XBTUSD"),
2218 create_test_perpetual_instrument_with_precisions(2, 0),
2219 );
2220
2221 let updates = parse_instrument_msg(&msg, &instruments_cache, UnixNanos::from(1));
2222
2223 assert_eq!(updates.len(), 1);
2224 match &updates[0] {
2225 Data::IndexPriceUpdate(update) => {
2226 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2227 assert_eq!(update.value, Price::from("75847.62"));
2228 }
2229 _ => panic!("Expected single IndexPriceUpdate for index-update wire shape"),
2230 }
2231 }
2232
2233 #[rstest]
2234 fn test_parse_funding_msg() {
2235 let json_data = load_test_json("ws_funding_rate.json");
2236 let msg: BitmexFundingMsg = serde_json::from_str(&json_data).unwrap();
2237 let update = parse_funding_msg(&msg, UnixNanos::from(1));
2238
2239 assert_eq!(update.instrument_id.to_string(), "XBTUSD.BITMEX");
2240 assert_eq!(update.rate.to_string(), "0.0001");
2241 assert_eq!(update.interval, Some(60 * 8));
2242 assert!(update.next_funding_ns.is_none());
2243 assert_eq!(update.ts_event, UnixNanos::from(1732507200000000000));
2244 assert_eq!(update.ts_init, UnixNanos::from(1));
2245 }
2246}