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::BookDelta(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::BookDelta(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::BookDepth10(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 cancel_reason.as_deref().map(Ustr::from),
759 );
760 Some(ParsedOrderEvent::Canceled(canceled))
761 }
762 }
763 BitmexOrderStatus::Expired => {
764 let expired = OrderExpired::new(
765 trader_id,
766 strategy_id,
767 instrument_id,
768 client_order_id,
769 UUID4::new(),
770 ts_event,
771 ts_init,
772 false,
773 Some(venue_order_id),
774 Some(account_id),
775 );
776 Some(ParsedOrderEvent::Expired(expired))
777 }
778 _ => None,
782 }
783}
784
785pub fn parse_order_update_msg(
789 msg: &BitmexOrderUpdateMsg,
790 instrument: &InstrumentAny,
791 account_id: AccountId,
792 ts_init: UnixNanos,
793) -> Option<OrderUpdated> {
794 let trader_id = TraderId::external();
796 let strategy_id = StrategyId::external();
797 let instrument_id = parse_instrument_id(msg.symbol?);
798 let venue_order_id = Some(VenueOrderId::new(msg.order_id.to_string()));
799 let client_order_id = msg
800 .cl_ord_id
801 .as_ref()
802 .map_or_else(ClientOrderId::external, ClientOrderId::new);
803
804 let quantity = match (msg.leaves_qty, msg.cum_qty) {
807 (Some(leaves), Some(cum)) => parse_contracts_quantity((leaves + cum) as u64, instrument),
808 _ => Quantity::zero(instrument.size_precision()),
809 };
810 let price = msg
811 .price
812 .value()
813 .copied()
814 .map(|p| Price::new(p, instrument.price_precision()));
815
816 let trigger_price = None;
818 let protection_price = None;
820
821 let event_id = UUID4::new();
822 let ts_event = parse_optional_datetime_to_unix_nanos(&msg.timestamp, "timestamp");
823
824 Some(OrderUpdated::new(
825 trader_id,
826 strategy_id,
827 instrument_id,
828 client_order_id,
829 quantity,
830 event_id,
831 ts_event,
832 ts_init,
833 false, venue_order_id,
835 Some(account_id),
836 price,
837 trigger_price,
838 protection_price,
839 false, ))
841}
842
843pub fn parse_execution_msg(
857 msg: BitmexExecutionMsg,
858 instrument: &InstrumentAny,
859 ts_init: UnixNanos,
860) -> Option<FillReport> {
861 let exec_type = msg.exec_type?;
862
863 match exec_type {
864 BitmexExecType::Trade | BitmexExecType::Liquidation => {}
866 BitmexExecType::Bankruptcy => {
867 log::warn!(
868 "Processing bankruptcy execution as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
869 msg.order_id,
870 msg.symbol,
871 );
872 }
873
874 BitmexExecType::Settlement => {
876 log::debug!(
877 "Settlement execution skipped (not a fill): applies quanto conversion/PnL transfer on contract settlement: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
878 msg.order_id,
879 msg.symbol,
880 );
881 return None;
882 }
883 BitmexExecType::TrialFill => {
884 log::warn!(
885 "Trial fill execution received (testnet only), not processed as fill: exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
886 msg.order_id,
887 msg.symbol,
888 );
889 return None;
890 }
891
892 BitmexExecType::Funding => {
894 log::debug!(
895 "Funding execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
896 msg.order_id,
897 msg.symbol,
898 );
899 return None;
900 }
901 BitmexExecType::Insurance => {
902 log::debug!(
903 "Insurance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
904 msg.order_id,
905 msg.symbol,
906 );
907 return None;
908 }
909 BitmexExecType::Rebalance => {
910 log::debug!(
911 "Rebalance execution skipped (not a fill): exec_type={exec_type:?}, order_id={:?}, symbol={:?}",
912 msg.order_id,
913 msg.symbol,
914 );
915 return None;
916 }
917
918 BitmexExecType::New
920 | BitmexExecType::Canceled
921 | BitmexExecType::CancelReject
922 | BitmexExecType::Replaced
923 | BitmexExecType::Rejected
924 | BitmexExecType::AmendReject
925 | BitmexExecType::Suspended
926 | BitmexExecType::Released
927 | BitmexExecType::TriggeredOrActivatedBySystem => {
928 log::debug!(
929 "Execution message skipped (order state change, not a fill): exec_type={exec_type:?}, order_id={:?}",
930 msg.order_id,
931 );
932 return None;
933 }
934
935 BitmexExecType::Unknown(ref type_str) => {
936 log::warn!(
937 "Unknown execution type received, skipping: exec_type={type_str}, order_id={:?}, symbol={:?}",
938 msg.order_id,
939 msg.symbol,
940 );
941 return None;
942 }
943 }
944
945 let account_id = bitmex_account_id(msg.account?);
946 let instrument_id = parse_instrument_id(msg.symbol?);
947 let venue_order_id = VenueOrderId::new(msg.order_id?.to_string());
948 let trade_id = TradeId::new(msg.trd_match_id?.to_string());
949 let side = msg.side?;
950 let order_side = OrderSide::from(BitmexSide::from(side));
951 let last_qty = parse_signed_contracts_quantity(msg.last_qty?, instrument);
952 let last_px = Price::new(msg.last_px?, instrument.price_precision());
953 let settlement_currency_str = msg.settl_currency.unwrap_or(Ustr::from("XBT"));
954 let mapped_currency = map_bitmex_currency(settlement_currency_str.as_str());
955 let currency = get_currency(&mapped_currency);
956 let commission = Money::new(msg.commission.unwrap_or(0.0), currency);
957 let liquidity_side = parse_liquidity_side(&msg.last_liquidity_ind);
958 let client_order_id = msg.cl_ord_id.map(ClientOrderId::new);
959 let venue_position_id = None; let ts_event = parse_optional_datetime_to_unix_nanos(&msg.transact_time, "transact_time");
961
962 Some(FillReport::new(
963 account_id,
964 instrument_id,
965 venue_order_id,
966 trade_id,
967 order_side,
968 last_qty,
969 last_px,
970 commission,
971 liquidity_side,
972 client_order_id,
973 venue_position_id,
974 ts_event,
975 ts_init,
976 None,
977 ))
978}
979
980#[must_use]
986pub fn parse_position_msg(
987 msg: &BitmexPositionMsg,
988 instrument: &InstrumentAny,
989 ts_init: UnixNanos,
990) -> PositionStatusReport {
991 let account_id = bitmex_account_id(msg.account);
992 let instrument_id = parse_instrument_id(msg.symbol);
993 let position_side = parse_position_side(msg.current_qty);
994 let quantity = parse_signed_contracts_quantity(msg.current_qty.unwrap_or(0), instrument);
995 let venue_position_id = None; let avg_px_open = msg
997 .avg_entry_price
998 .and_then(|p| Decimal::from_str(&p.to_string()).ok());
999 let ts_last = parse_optional_datetime_to_unix_nanos(&msg.timestamp, "timestamp");
1000
1001 PositionStatusReport::new(
1002 account_id,
1003 instrument_id,
1004 position_side,
1005 quantity,
1006 ts_last,
1007 ts_init,
1008 None, venue_position_id, avg_px_open, )
1012}
1013
1014#[must_use]
1027pub fn parse_instrument_msg(
1028 msg: &BitmexInstrumentMsg,
1029 instruments_cache: &AHashMap<Ustr, InstrumentAny>,
1030 ts_init: UnixNanos,
1031) -> Vec<Data> {
1032 let mut updates = Vec::new();
1033 let is_index = is_index_symbol(&msg.symbol);
1034
1035 let effective_mark_price = msg.mark_price.or(msg.fair_price);
1038 let effective_index_price = if is_index {
1039 msg.last_price
1040 } else {
1041 msg.indicative_settle_price.or(msg.index_price)
1042 };
1043
1044 if effective_mark_price.is_none() && effective_index_price.is_none() {
1045 return updates;
1046 }
1047
1048 let instrument_id = InstrumentId::new(Symbol::from_ustr_unchecked(msg.symbol), *BITMEX_VENUE);
1049 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "");
1050
1051 let price_precision = match instruments_cache.get(&msg.symbol) {
1053 Some(instrument) => instrument.price_precision(),
1054 None => {
1055 if is_index {
1059 log::trace!(
1060 "Index instrument {} not in cache, skipping update",
1061 msg.symbol
1062 );
1063 } else {
1064 log::debug!("Instrument {} not in cache, skipping update", msg.symbol);
1065 }
1066 return updates;
1067 }
1068 };
1069
1070 if let Some(mark_price) = effective_mark_price {
1073 let price = Price::new(mark_price, price_precision);
1074 updates.push(Data::MarkPrice(MarkPriceUpdate::new(
1075 instrument_id,
1076 price,
1077 ts_event,
1078 ts_init,
1079 )));
1080 }
1081
1082 if let Some(index_price) = effective_index_price {
1084 let price = Price::new(index_price, price_precision);
1085 updates.push(Data::IndexPrice(IndexPriceUpdate::new(
1086 instrument_id,
1087 price,
1088 ts_event,
1089 ts_init,
1090 )));
1091 }
1092
1093 updates
1094}
1095
1096#[must_use]
1102pub fn parse_funding_msg(msg: &BitmexFundingMsg, ts_init: UnixNanos) -> FundingRateUpdate {
1103 let instrument_id = InstrumentId::from(format!("{}.BITMEX", msg.symbol));
1104 let funding_interval = Offset::UTC.to_datetime(msg.funding_interval);
1105 let interval_hours = u16::from(funding_interval.hour().cast_unsigned());
1106 let interval_minutes = u16::from(funding_interval.minute().cast_unsigned());
1107 let interval = Some(interval_hours * 60 + interval_minutes);
1108 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "");
1109
1110 FundingRateUpdate::new(
1111 instrument_id,
1112 msg.funding_rate,
1113 interval,
1114 None, ts_event,
1116 ts_init,
1117 )
1118}
1119
1120#[must_use]
1129pub fn parse_wallet_msg(msg: &BitmexWalletMsg, ts_init: UnixNanos) -> AccountState {
1130 let account_id = bitmex_account_id(msg.account);
1131
1132 let currency_str = map_bitmex_currency(msg.currency.as_str());
1134 let currency = get_currency(¤cy_str);
1135
1136 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1139 let amount_dec = Decimal::from(msg.amount.unwrap_or(0)) / divisor;
1140
1141 let balance = AccountBalance::from_total_and_locked(amount_dec, Decimal::ZERO, currency)
1142 .expect("Balance calculation should be valid");
1143
1144 AccountState::new(
1145 account_id,
1146 AccountType::Margin,
1147 vec![balance],
1148 vec![], true, UUID4::new(),
1151 ts_init,
1152 ts_init,
1153 None,
1154 )
1155}
1156
1157#[must_use]
1159pub fn parse_margin_msg(msg: &BitmexMarginMsg) -> MarginBalance {
1160 let currency_str = map_bitmex_currency(msg.currency.as_str());
1161 let currency = get_currency(¤cy_str);
1162
1163 let divisor = bitmex_currency_divisor(msg.currency.as_str());
1164 let initial_dec = Decimal::from(msg.init_margin.unwrap_or(0).max(0)) / divisor;
1165 let maintenance_dec = Decimal::from(msg.maint_margin.unwrap_or(0).max(0)) / divisor;
1166
1167 MarginBalance::new(
1168 Money::from_decimal(initial_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1169 Money::from_decimal(maintenance_dec, currency).unwrap_or_else(|_| Money::zero(currency)),
1170 None,
1171 )
1172}
1173
1174#[must_use]
1176pub fn parse_margin_account_state(msg: &BitmexMarginMsg, ts_init: UnixNanos) -> AccountState {
1177 let account_id = bitmex_account_id(msg.account);
1178 let balance = parse_account_balance(msg);
1179
1180 let margin = parse_margin_msg(msg);
1181
1182 let margins = if !margin.initial.is_zero() || !margin.maintenance.is_zero() {
1183 vec![margin]
1184 } else {
1185 vec![]
1186 };
1187
1188 let ts_event = parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "margin.timestamp");
1189
1190 AccountState::new(
1191 account_id,
1192 AccountType::Margin,
1193 vec![balance],
1194 margins,
1195 true,
1196 UUID4::new(),
1197 ts_event,
1198 ts_init,
1199 None,
1200 )
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205 use jiff::Timestamp;
1206 use nautilus_model::{
1207 enums::{AggressorSide, BookAction, LiquiditySide, PositionSide},
1208 identifiers::Symbol,
1209 instruments::crypto_perpetual::CryptoPerpetual,
1210 };
1211 use rstest::rstest;
1212 use ustr::Ustr;
1213
1214 use super::*;
1215 use crate::common::{
1216 enums::{BitmexExecType, BitmexOrderStatus},
1217 testing::load_test_json,
1218 };
1219
1220 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, "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}