1use std::str::FromStr;
19
20use dashmap::DashMap;
21use nautilus_core::{UnixNanos, uuid::UUID4};
22use nautilus_model::{
23 data::{Bar, BarType, TradeTick},
24 enums::{ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType},
25 identifiers::{ClientOrderId, OrderListId, Symbol, TradeId, VenueOrderId},
26 instruments::{
27 CryptoFuture, CryptoFuturesSpread, CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
28 },
29 reports::{FillReport, OrderStatusReport, PositionStatusReport},
30 types::{Currency, Money, Price, Quantity, fixed::FIXED_PRECISION},
31};
32use rust_decimal::Decimal;
33use ustr::Ustr;
34
35use super::models::{
36 BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTrade, BitmexTradeBin,
37};
38use crate::common::{
39 enums::{
40 BitmexExecInstruction, BitmexExecType, BitmexInstrumentState, BitmexInstrumentType,
41 BitmexOrderType, BitmexPegPriceType,
42 },
43 parse::{
44 bitmex_account_id, clean_reason, convert_contract_quantity,
45 derive_contract_decimal_and_increment, derive_trade_id, extract_trigger_type,
46 map_bitmex_currency, normalize_trade_bin_prices, normalize_trade_bin_volume,
47 parse_aggressor_side, parse_contracts_quantity, parse_instrument_id, parse_liquidity_side,
48 parse_optional_datetime_to_unix_nanos, parse_position_side,
49 parse_signed_contracts_quantity,
50 },
51};
52
53#[derive(Debug)]
55pub enum InstrumentParseResult {
56 Ok(Box<InstrumentAny>),
58 Unsupported {
60 symbol: String,
61 instrument_type: BitmexInstrumentType,
62 },
63 Inactive {
65 symbol: String,
66 state: BitmexInstrumentState,
67 },
68 Failed {
70 symbol: String,
71 instrument_type: BitmexInstrumentType,
72 error: String,
73 },
74}
75
76fn get_position_multiplier(definition: &BitmexInstrument) -> Option<f64> {
82 if definition.is_inverse {
83 definition
84 .underlying_to_settle_multiplier
85 .or(definition.underlying_to_position_multiplier)
86 } else {
87 definition.underlying_to_position_multiplier
88 }
89}
90
91#[must_use]
93pub fn parse_instrument_any(
94 instrument: &BitmexInstrument,
95 ts_init: UnixNanos,
96) -> InstrumentParseResult {
97 let symbol = instrument.symbol.to_string();
98 let instrument_type = instrument.instrument_type;
99
100 match instrument.state {
101 BitmexInstrumentState::Open | BitmexInstrumentState::Closed => {}
102 state @ (BitmexInstrumentState::Unlisted
103 | BitmexInstrumentState::Settled
104 | BitmexInstrumentState::Delisted
105 | BitmexInstrumentState::Unknown) => {
106 return InstrumentParseResult::Inactive { symbol, state };
107 }
108 }
109
110 match instrument.instrument_type {
111 BitmexInstrumentType::Spot => match parse_spot_instrument(instrument, ts_init) {
112 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
113 Err(e) => InstrumentParseResult::Failed {
114 symbol,
115 instrument_type,
116 error: e.to_string(),
117 },
118 },
119 BitmexInstrumentType::PerpetualContract | BitmexInstrumentType::PerpetualContractFx => {
120 match parse_perpetual_instrument(instrument, ts_init) {
122 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
123 Err(e) => InstrumentParseResult::Failed {
124 symbol,
125 instrument_type,
126 error: e.to_string(),
127 },
128 }
129 }
130 BitmexInstrumentType::Futures => match parse_futures_instrument(instrument, ts_init) {
131 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
132 Err(e) => InstrumentParseResult::Failed {
133 symbol,
134 instrument_type,
135 error: e.to_string(),
136 },
137 },
138 BitmexInstrumentType::FuturesSpread | BitmexInstrumentType::FuturesSpreads => {
139 match parse_crypto_futures_spread_instrument(instrument, ts_init) {
140 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
141 Err(e) => InstrumentParseResult::Failed {
142 symbol,
143 instrument_type,
144 error: e.to_string(),
145 },
146 }
147 }
148 BitmexInstrumentType::PredictionMarket
149 | BitmexInstrumentType::LegacyFutures
150 | BitmexInstrumentType::LegacyFuturesN => {
151 match parse_futures_instrument(instrument, ts_init) {
153 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
154 Err(e) => InstrumentParseResult::Failed {
155 symbol,
156 instrument_type,
157 error: e.to_string(),
158 },
159 }
160 }
161 BitmexInstrumentType::BasketIndex
162 | BitmexInstrumentType::CryptoIndex
163 | BitmexInstrumentType::FxIndex
164 | BitmexInstrumentType::LendingIndex
165 | BitmexInstrumentType::ReferenceBasket
166 | BitmexInstrumentType::VolatilityIndex
167 | BitmexInstrumentType::StockIndex
168 | BitmexInstrumentType::YieldIndex => {
169 match parse_index_instrument(instrument, ts_init) {
172 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
173 Err(e) => InstrumentParseResult::Failed {
174 symbol,
175 instrument_type,
176 error: e.to_string(),
177 },
178 }
179 }
180
181 BitmexInstrumentType::TradFiPerpetual
186 | BitmexInstrumentType::CallOption
187 | BitmexInstrumentType::PutOption
188 | BitmexInstrumentType::SwapRate
189 | BitmexInstrumentType::Other => InstrumentParseResult::Unsupported {
190 symbol,
191 instrument_type,
192 },
193 }
194}
195
196pub fn parse_index_instrument(
205 definition: &BitmexInstrument,
206 ts_init: UnixNanos,
207) -> anyhow::Result<InstrumentAny> {
208 let instrument_id = parse_instrument_id(definition.symbol);
209 let raw_symbol = Symbol::new(definition.symbol);
210
211 let base_currency = Currency::USD();
212 let quote_currency = Currency::USD();
213 let settlement_currency = Currency::USD();
214
215 let price_increment = Price::from(definition.tick_size.to_string());
216 let size_increment = Quantity::from(1); Ok(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
219 instrument_id,
220 raw_symbol,
221 base_currency,
222 quote_currency,
223 settlement_currency,
224 false, price_increment.precision,
226 size_increment.precision,
227 price_increment,
228 size_increment,
229 None, None, None, None, None, None, None, None, None, None, None, None, None, None, ts_init,
244 ts_init,
245 )))
246}
247
248pub fn parse_spot_instrument(
254 definition: &BitmexInstrument,
255 ts_init: UnixNanos,
256) -> anyhow::Result<InstrumentAny> {
257 let instrument_id = parse_instrument_id(definition.symbol);
258 let raw_symbol = Symbol::new(definition.symbol);
259 let base_currency = get_currency(&definition.underlying.to_uppercase());
260 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
261
262 let price_increment = Price::from(definition.tick_size.to_string());
263
264 let max_scale = FIXED_PRECISION as u32;
265 let (contract_decimal, size_increment) =
266 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
267
268 let min_quantity = convert_contract_quantity(
269 definition.lot_size,
270 contract_decimal,
271 max_scale,
272 "minimum quantity",
273 )?;
274
275 let taker_fee = definition
276 .taker_fee
277 .and_then(|fee| Decimal::try_from(fee).ok())
278 .unwrap_or(Decimal::ZERO);
279 let maker_fee = definition
280 .maker_fee
281 .and_then(|fee| Decimal::try_from(fee).ok())
282 .unwrap_or(Decimal::ZERO);
283
284 let margin_init = definition
285 .init_margin
286 .as_ref()
287 .and_then(|margin| Decimal::try_from(*margin).ok())
288 .unwrap_or(Decimal::ZERO);
289 let margin_maint = definition
290 .maint_margin
291 .as_ref()
292 .and_then(|margin| Decimal::try_from(*margin).ok())
293 .unwrap_or(Decimal::ZERO);
294
295 let lot_size =
296 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
297 let max_quantity = convert_contract_quantity(
298 definition.max_order_qty,
299 contract_decimal,
300 max_scale,
301 "max quantity",
302 )?;
303 let max_notional: Option<Money> = None;
304 let min_notional: Option<Money> = None;
305 let max_price = definition
306 .max_price
307 .map(|price| Price::from(price.to_string()));
308 let min_price = definition
309 .min_price
310 .map(|price| Price::from(price.to_string()));
311 let ts_event = UnixNanos::from(definition.timestamp);
312
313 let instrument = CurrencyPair::new(
314 instrument_id,
315 raw_symbol,
316 base_currency,
317 quote_currency,
318 price_increment.precision,
319 size_increment.precision,
320 price_increment,
321 size_increment,
322 None, lot_size,
324 max_quantity,
325 min_quantity,
326 max_notional,
327 min_notional,
328 max_price,
329 min_price,
330 Some(margin_init),
331 Some(margin_maint),
332 Some(maker_fee),
333 Some(taker_fee),
334 None,
335 None, ts_event,
337 ts_init,
338 );
339
340 Ok(InstrumentAny::CurrencyPair(instrument))
341}
342
343pub fn parse_perpetual_instrument(
349 definition: &BitmexInstrument,
350 ts_init: UnixNanos,
351) -> anyhow::Result<InstrumentAny> {
352 let instrument_id = parse_instrument_id(definition.symbol);
353 let raw_symbol = Symbol::new(definition.symbol);
354 let base_currency = get_currency(&definition.underlying.to_uppercase());
355 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
356 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
357 || definition.quote_currency.to_uppercase(),
358 |s| s.to_uppercase(),
359 ));
360 let is_inverse = definition.is_inverse;
361
362 let price_increment = Price::from(definition.tick_size.to_string());
363
364 let max_scale = FIXED_PRECISION as u32;
365 let (contract_decimal, size_increment) =
366 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
367
368 let lot_size =
369 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
370
371 let taker_fee = definition
372 .taker_fee
373 .and_then(|fee| Decimal::try_from(fee).ok())
374 .unwrap_or(Decimal::ZERO);
375 let maker_fee = definition
376 .maker_fee
377 .and_then(|fee| Decimal::try_from(fee).ok())
378 .unwrap_or(Decimal::ZERO);
379
380 let margin_init = definition
381 .init_margin
382 .as_ref()
383 .and_then(|margin| Decimal::try_from(*margin).ok())
384 .unwrap_or(Decimal::ZERO);
385 let margin_maint = definition
386 .maint_margin
387 .as_ref()
388 .and_then(|margin| Decimal::try_from(*margin).ok())
389 .unwrap_or(Decimal::ZERO);
390
391 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
393 let max_quantity = convert_contract_quantity(
394 definition.max_order_qty,
395 contract_decimal,
396 max_scale,
397 "max quantity",
398 )?;
399 let min_quantity = lot_size;
400 let max_notional: Option<Money> = None;
401 let min_notional: Option<Money> = None;
402 let max_price = definition
403 .max_price
404 .map(|price| Price::from(price.to_string()));
405 let min_price = definition
406 .min_price
407 .map(|price| Price::from(price.to_string()));
408 let ts_event = UnixNanos::from(definition.timestamp);
409
410 let instrument = CryptoPerpetual::new(
411 instrument_id,
412 raw_symbol,
413 base_currency,
414 quote_currency,
415 settlement_currency,
416 is_inverse,
417 price_increment.precision,
418 size_increment.precision,
419 price_increment,
420 size_increment,
421 multiplier,
422 lot_size,
423 max_quantity,
424 min_quantity,
425 max_notional,
426 min_notional,
427 max_price,
428 min_price,
429 Some(margin_init),
430 Some(margin_maint),
431 Some(maker_fee),
432 Some(taker_fee),
433 None,
434 None, ts_event,
436 ts_init,
437 );
438
439 Ok(InstrumentAny::CryptoPerpetual(instrument))
440}
441
442pub fn parse_futures_instrument(
448 definition: &BitmexInstrument,
449 ts_init: UnixNanos,
450) -> anyhow::Result<InstrumentAny> {
451 let instrument_id = parse_instrument_id(definition.symbol);
452 let raw_symbol = Symbol::new(definition.symbol);
453 let underlying = get_currency(&definition.underlying.to_uppercase());
454 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
455 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
456 || definition.quote_currency.to_uppercase(),
457 |s| s.to_uppercase(),
458 ));
459 let is_inverse = definition.is_inverse;
460
461 let ts_event = UnixNanos::from(definition.timestamp);
462 let activation_ns = definition
463 .listing
464 .as_ref()
465 .map_or(ts_event, |dt| UnixNanos::from(*dt));
466 let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
467 let price_increment = Price::from(definition.tick_size.to_string());
468
469 let max_scale = FIXED_PRECISION as u32;
470 let (contract_decimal, size_increment) =
471 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
472
473 let lot_size =
474 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
475
476 let taker_fee = definition
477 .taker_fee
478 .and_then(|fee| Decimal::try_from(fee).ok())
479 .unwrap_or(Decimal::ZERO);
480 let maker_fee = definition
481 .maker_fee
482 .and_then(|fee| Decimal::try_from(fee).ok())
483 .unwrap_or(Decimal::ZERO);
484
485 let margin_init = definition
486 .init_margin
487 .as_ref()
488 .and_then(|margin| Decimal::try_from(*margin).ok())
489 .unwrap_or(Decimal::ZERO);
490 let margin_maint = definition
491 .maint_margin
492 .as_ref()
493 .and_then(|margin| Decimal::try_from(*margin).ok())
494 .unwrap_or(Decimal::ZERO);
495
496 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
498
499 let max_quantity = convert_contract_quantity(
500 definition.max_order_qty,
501 contract_decimal,
502 max_scale,
503 "max quantity",
504 )?;
505 let min_quantity = lot_size;
506 let max_notional: Option<Money> = None;
507 let min_notional: Option<Money> = None;
508 let max_price = definition
509 .max_price
510 .map(|price| Price::from(price.to_string()));
511 let min_price = definition
512 .min_price
513 .map(|price| Price::from(price.to_string()));
514
515 let instrument = CryptoFuture::new(
516 instrument_id,
517 raw_symbol,
518 underlying,
519 quote_currency,
520 settlement_currency,
521 is_inverse,
522 activation_ns,
523 expiration_ns,
524 price_increment.precision,
525 size_increment.precision,
526 price_increment,
527 size_increment,
528 multiplier,
529 lot_size,
530 max_quantity,
531 min_quantity,
532 max_notional,
533 min_notional,
534 max_price,
535 min_price,
536 Some(margin_init),
537 Some(margin_maint),
538 Some(maker_fee),
539 Some(taker_fee),
540 None,
541 None, ts_event,
543 ts_init,
544 );
545
546 Ok(InstrumentAny::CryptoFuture(instrument))
547}
548
549pub fn parse_crypto_futures_spread_instrument(
555 definition: &BitmexInstrument,
556 ts_init: UnixNanos,
557) -> anyhow::Result<InstrumentAny> {
558 let instrument_id = parse_instrument_id(definition.symbol);
559 let raw_symbol = Symbol::new(definition.symbol);
560 let underlying = get_currency(&definition.underlying.to_uppercase());
561 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
562 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
563 || definition.quote_currency.to_uppercase(),
564 |s| s.to_uppercase(),
565 ));
566 let is_inverse = definition.is_inverse;
567
568 let ts_event = UnixNanos::from(definition.timestamp);
569 let activation_ns = definition
570 .listing
571 .as_ref()
572 .map_or(ts_event, |dt| UnixNanos::from(*dt));
573 let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
574 let price_increment = Price::from(definition.tick_size.to_string());
575
576 let max_scale = FIXED_PRECISION as u32;
577 let (contract_decimal, size_increment) =
578 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
579
580 let lot_size =
581 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
582
583 let taker_fee = definition
584 .taker_fee
585 .and_then(|fee| Decimal::try_from(fee).ok())
586 .unwrap_or(Decimal::ZERO);
587 let maker_fee = definition
588 .maker_fee
589 .and_then(|fee| Decimal::try_from(fee).ok())
590 .unwrap_or(Decimal::ZERO);
591
592 let margin_init = definition
593 .init_margin
594 .as_ref()
595 .and_then(|margin| Decimal::try_from(*margin).ok())
596 .unwrap_or(Decimal::ZERO);
597 let margin_maint = definition
598 .maint_margin
599 .as_ref()
600 .and_then(|margin| Decimal::try_from(*margin).ok())
601 .unwrap_or(Decimal::ZERO);
602
603 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
604 let max_quantity = convert_contract_quantity(
605 definition.max_order_qty,
606 contract_decimal,
607 max_scale,
608 "max quantity",
609 )?;
610 let min_quantity = lot_size;
611 let max_notional: Option<Money> = None;
612 let min_notional: Option<Money> = None;
613 let max_price = definition
614 .max_price
615 .map(|price| Price::from(price.to_string()));
616 let min_price = definition
617 .min_price
618 .map(|price| Price::from(price.to_string()));
619
620 let instrument = CryptoFuturesSpread::new(
621 instrument_id,
622 raw_symbol,
623 underlying,
624 quote_currency,
625 settlement_currency,
626 is_inverse,
627 Ustr::from("FS"),
628 activation_ns,
629 expiration_ns,
630 price_increment.precision,
631 size_increment.precision,
632 price_increment,
633 size_increment,
634 multiplier,
635 lot_size,
636 max_quantity,
637 min_quantity,
638 max_notional,
639 min_notional,
640 max_price,
641 min_price,
642 Some(margin_init),
643 Some(margin_maint),
644 Some(maker_fee),
645 Some(taker_fee),
646 None,
647 None,
648 ts_event,
649 ts_init,
650 );
651
652 Ok(InstrumentAny::CryptoFuturesSpread(instrument))
653}
654
655pub fn parse_trade(
662 trade: &BitmexTrade,
663 instrument: &InstrumentAny,
664 ts_init: UnixNanos,
665) -> anyhow::Result<TradeTick> {
666 let instrument_id = parse_instrument_id(trade.symbol);
667 let price = Price::new(trade.price, instrument.price_precision());
668 let size = parse_contracts_quantity(trade.size as u64, instrument);
669 let aggressor_side = parse_aggressor_side(&trade.side);
670 let ts_event = UnixNanos::from(trade.timestamp);
671 let trade_id = match trade.trd_match_id {
672 Some(uuid) => TradeId::new(uuid.to_string()),
673 None => derive_trade_id(
674 trade.symbol,
675 ts_event.as_u64(),
676 trade.price,
677 trade.size,
678 trade.side,
679 ),
680 };
681
682 Ok(TradeTick::new(
683 instrument_id,
684 price,
685 size,
686 aggressor_side,
687 trade_id,
688 ts_event,
689 ts_init,
690 ))
691}
692
693pub fn parse_trade_bin(
699 bin: &BitmexTradeBin,
700 instrument: &InstrumentAny,
701 bar_type: &BarType,
702 ts_init: UnixNanos,
703) -> anyhow::Result<Bar> {
704 let instrument_id = bar_type.instrument_id();
705 let price_precision = instrument.price_precision();
706
707 let open = bin
708 .open
709 .ok_or_else(|| anyhow::anyhow!("Trade bin missing open price for {instrument_id}"))?;
710 let high = bin
711 .high
712 .ok_or_else(|| anyhow::anyhow!("Trade bin missing high price for {instrument_id}"))?;
713 let low = bin
714 .low
715 .ok_or_else(|| anyhow::anyhow!("Trade bin missing low price for {instrument_id}"))?;
716 let close = bin
717 .close
718 .ok_or_else(|| anyhow::anyhow!("Trade bin missing close price for {instrument_id}"))?;
719
720 let open = Price::new(open, price_precision);
721 let high = Price::new(high, price_precision);
722 let low = Price::new(low, price_precision);
723 let close = Price::new(close, price_precision);
724
725 let (open, high, low, close) =
726 normalize_trade_bin_prices(open, high, low, close, &bin.symbol, Some(bar_type));
727
728 let volume_contracts = normalize_trade_bin_volume(bin.volume, &bin.symbol);
729 let volume = parse_contracts_quantity(volume_contracts, instrument);
730 let ts_event = UnixNanos::from(bin.timestamp);
731
732 Ok(Bar::new(
733 *bar_type, open, high, low, close, volume, ts_event, ts_init,
734 ))
735}
736
737pub fn parse_order_status_report(
754 order: &BitmexOrder,
755 instrument: &InstrumentAny,
756 order_type_cache: &DashMap<ClientOrderId, OrderType>,
757 ts_init: UnixNanos,
758) -> anyhow::Result<OrderStatusReport> {
759 let instrument_id = instrument.id();
760 let account_id = bitmex_account_id(order.account);
761 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
762 let order_side: OrderSide = order
763 .side
764 .map_or(OrderSide::NoOrderSide, |side| side.into());
765
766 let order_type: OrderType = order.ord_type.map_or_else(
769 || {
770 if let Some(cl_ord_id) = &order.cl_ord_id {
771 let client_order_id = ClientOrderId::new(cl_ord_id);
772 if let Some(cached_type) = order_type_cache.get(&client_order_id) {
773 log::debug!(
774 "Using cached ord_type={:?} for order {}",
775 *cached_type,
776 order.order_id,
777 );
778 return *cached_type;
779 }
780 }
781
782 let inferred = if order.stop_px.is_some() {
783 if order.price.is_some() {
784 OrderType::StopLimit
785 } else {
786 OrderType::StopMarket
787 }
788 } else if order.price.is_some() {
789 OrderType::Limit
790 } else {
791 OrderType::Market
792 };
793 log::debug!(
794 "Inferred ord_type={inferred:?} for order {} (price={:?}, stop_px={:?})",
795 order.order_id,
796 order.price,
797 order.stop_px,
798 );
799 inferred
800 },
801 |t| {
802 if t == BitmexOrderType::Pegged
804 && order.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
805 {
806 if order.price.is_some() {
807 OrderType::TrailingStopLimit
808 } else {
809 OrderType::TrailingStopMarket
810 }
811 } else {
812 t.into()
813 }
814 },
815 );
816
817 let time_in_force: TimeInForce = order
820 .time_in_force
821 .and_then(|tif| tif.try_into().ok())
822 .unwrap_or(TimeInForce::Gtc);
823
824 let order_status: OrderStatus = if let Some(status) = order.ord_status.as_ref() {
827 (*status).into()
828 } else {
829 match (order.leaves_qty, order.cum_qty, order.working_indicator) {
831 (Some(0), Some(cum), _) if cum > 0 => {
832 log::debug!(
833 "Inferred Filled from missing ordStatus (leaves_qty=0, cum_qty>0): order_id={:?}, client_order_id={:?}, cum_qty={}",
834 order.order_id,
835 order.cl_ord_id,
836 cum,
837 );
838 OrderStatus::Filled
839 }
840 (Some(0), _, _) => {
841 log::debug!(
842 "Inferred Canceled from missing ordStatus (leaves_qty=0, cum_qty<=0): order_id={:?}, client_order_id={:?}, cum_qty={:?}",
843 order.order_id,
844 order.cl_ord_id,
845 order.cum_qty,
846 );
847 OrderStatus::Canceled
848 }
849 (None, None, Some(false)) => {
851 log::debug!(
852 "Inferred Canceled from missing ordStatus with working_indicator=false: order_id={:?}, client_order_id={:?}",
853 order.order_id,
854 order.cl_ord_id,
855 );
856 OrderStatus::Canceled
857 }
858 _ => {
859 let order_json = serde_json::to_string(order)?;
860 anyhow::bail!(
861 "Order missing ord_status and cannot infer (order_id={}, client_order_id={:?}, leaves_qty={:?}, cum_qty={:?}, working_indicator={:?}, order_json={})",
862 order.order_id,
863 order.cl_ord_id,
864 order.leaves_qty,
865 order.cum_qty,
866 order.working_indicator,
867 order_json
868 );
869 }
870 }
871 };
872
873 let (quantity, filled_qty) = if let Some(qty) = order.order_qty {
875 let quantity = parse_signed_contracts_quantity(qty, instrument);
876 let filled_qty = parse_signed_contracts_quantity(order.cum_qty.unwrap_or(0), instrument);
877 (quantity, filled_qty)
878 } else if let (Some(cum), Some(leaves)) = (order.cum_qty, order.leaves_qty) {
879 log::debug!(
880 "Reconstructing order_qty from cum_qty + leaves_qty: order_id={:?}, client_order_id={:?}, cum_qty={}, leaves_qty={}",
881 order.order_id,
882 order.cl_ord_id,
883 cum,
884 leaves,
885 );
886 let quantity = parse_signed_contracts_quantity(cum + leaves, instrument);
887 let filled_qty = parse_signed_contracts_quantity(cum, instrument);
888 (quantity, filled_qty)
889 } else if order_status == OrderStatus::Canceled || order_status == OrderStatus::Rejected {
890 log::debug!(
893 "Order missing quantity fields, using 0 for both (will be reconciled from cache): order_id={:?}, client_order_id={:?}, status={:?}",
894 order.order_id,
895 order.cl_ord_id,
896 order_status,
897 );
898 let zero_qty = Quantity::zero(instrument.size_precision());
899 (zero_qty, zero_qty)
900 } else {
901 anyhow::bail!(
902 "Order missing order_qty and cannot reconstruct (order_id={}, cum_qty={:?}, leaves_qty={:?})",
903 order.order_id,
904 order.cum_qty,
905 order.leaves_qty
906 );
907 };
908 let report_id = UUID4::new();
909 let ts_accepted = order.transact_time.map_or(ts_init, UnixNanos::from);
910 let ts_last = order.timestamp.map_or(ts_init, UnixNanos::from);
911
912 let mut report = OrderStatusReport::new(
913 account_id,
914 instrument_id,
915 None, venue_order_id,
917 order_side,
918 order_type,
919 time_in_force,
920 order_status,
921 quantity,
922 filled_qty,
923 ts_accepted,
924 ts_last,
925 ts_init,
926 Some(report_id),
927 );
928
929 if let Some(cl_ord_id) = order.cl_ord_id {
930 report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
931 }
932
933 if let Some(cl_ord_link_id) = order.cl_ord_link_id {
934 report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
935 }
936
937 let price_precision = instrument.price_precision();
938
939 if let Some(price) = order.price {
940 report = report.with_price(Price::new(price, price_precision));
941 }
942
943 if let Some(avg_px) = order.avg_px {
944 report = report.with_avg_px(avg_px)?;
945 }
946
947 if let Some(trigger_price) = order.stop_px {
948 report = report
949 .with_trigger_price(Price::new(trigger_price, price_precision))
950 .with_trigger_type(extract_trigger_type(order.exec_inst.as_ref()));
951 }
952
953 if matches!(
955 order_type,
956 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
957 ) && let Some(peg_offset) = order.peg_offset_value
958 {
959 let trailing_offset = Decimal::try_from(peg_offset.abs())
960 .unwrap_or_else(|_| Decimal::new(peg_offset.abs() as i64, 0));
961 report = report
962 .with_trailing_offset(trailing_offset)
963 .with_trailing_offset_type(TrailingOffsetType::Price);
964
965 if order.stop_px.is_none() {
966 report = report.with_trigger_type(extract_trigger_type(order.exec_inst.as_ref()));
967 }
968 }
969
970 if let Some(exec_instructions) = &order.exec_inst {
971 for inst in exec_instructions {
972 match inst {
973 BitmexExecInstruction::ParticipateDoNotInitiate => {
974 report = report.with_post_only(true);
975 }
976 BitmexExecInstruction::ReduceOnly => report = report.with_reduce_only(true),
977 BitmexExecInstruction::LastPrice
978 | BitmexExecInstruction::Close
979 | BitmexExecInstruction::MarkPrice
980 | BitmexExecInstruction::IndexPrice
981 | BitmexExecInstruction::AllOrNone
982 | BitmexExecInstruction::Fixed
983 | BitmexExecInstruction::Unknown => {}
984 }
985 }
986 }
987
988 if let Some(contingency_type) = order.contingency_type {
989 report = report.with_contingency_type(contingency_type.into());
990 }
991
992 if matches!(
993 report.contingency_type,
994 ContingencyType::Oco | ContingencyType::Oto | ContingencyType::Ouo
995 ) && report.order_list_id.is_none()
996 {
997 log::debug!(
998 "BitMEX order missing clOrdLinkID for contingent order: order_id={}, client_order_id={:?}, contingency_type={:?}",
999 order.order_id,
1000 report.client_order_id,
1001 report.contingency_type,
1002 );
1003 }
1004
1005 if order_status == OrderStatus::Rejected {
1007 if let Some(reason) = order.ord_rej_reason.or(order.text) {
1008 log::debug!(
1009 "Order rejected with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
1010 order.order_id,
1011 order.cl_ord_id,
1012 reason,
1013 );
1014 report = report.with_cancel_reason(clean_reason(reason.as_ref()));
1015 } else {
1016 log::debug!(
1017 "Order rejected without reason from BitMEX: order_id={:?}, client_order_id={:?}, ord_status={:?}, ord_rej_reason={:?}, text={:?}",
1018 order.order_id,
1019 order.cl_ord_id,
1020 order.ord_status,
1021 order.ord_rej_reason,
1022 order.text,
1023 );
1024 }
1025 } else if order_status == OrderStatus::Canceled
1026 && let Some(reason) = order.ord_rej_reason.or(order.text)
1027 {
1028 log::trace!(
1029 "Order canceled with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
1030 order.order_id,
1031 order.cl_ord_id,
1032 reason,
1033 );
1034 report = report.with_cancel_reason(clean_reason(reason.as_ref()));
1035 }
1036
1037 Ok(report)
1040}
1041
1042pub fn parse_fill_report(
1055 exec: &BitmexExecution,
1056 instrument: &InstrumentAny,
1057 ts_init: UnixNanos,
1058) -> anyhow::Result<FillReport> {
1059 if !matches!(exec.exec_type, BitmexExecType::Trade) {
1062 anyhow::bail!("Skipping non-trade execution: {:?}", exec.exec_type);
1063 }
1064
1065 let order_id = exec.order_id.ok_or_else(|| {
1067 anyhow::anyhow!("Skipping execution without order_id: {:?}", exec.exec_type)
1068 })?;
1069
1070 let account_id = bitmex_account_id(exec.account);
1071 let instrument_id = instrument.id();
1072 let venue_order_id = VenueOrderId::new(order_id.to_string());
1073 let trade_id = TradeId::new(
1075 exec.trd_match_id
1076 .or(Some(exec.exec_id))
1077 .ok_or_else(|| anyhow::anyhow!("Fill missing both trd_match_id and exec_id"))?
1078 .to_string(),
1079 );
1080 let Some(side) = exec.side else {
1082 anyhow::bail!("Skipping execution without side: {:?}", exec.exec_type);
1083 };
1084 let order_side: OrderSide = side.into();
1085 let last_qty = parse_signed_contracts_quantity(exec.last_qty, instrument);
1086 let last_px = Price::new(exec.last_px, instrument.price_precision());
1087
1088 let settlement_currency_str = exec.settl_currency.unwrap_or(Ustr::from("XBT")).as_str();
1090 let mapped_currency = map_bitmex_currency(settlement_currency_str);
1091 let currency = get_currency(&mapped_currency);
1092 let commission = Money::new(exec.commission.unwrap_or(0.0), currency);
1093 let liquidity_side = parse_liquidity_side(&exec.last_liquidity_ind);
1094 let client_order_id = exec.cl_ord_id.map(ClientOrderId::new);
1095 let venue_position_id = None; let ts_event = exec.transact_time.map_or(ts_init, UnixNanos::from);
1097
1098 Ok(FillReport::new(
1099 account_id,
1100 instrument_id,
1101 venue_order_id,
1102 trade_id,
1103 order_side,
1104 last_qty,
1105 last_px,
1106 commission,
1107 liquidity_side,
1108 client_order_id,
1109 venue_position_id,
1110 ts_event,
1111 ts_init,
1112 None,
1113 ))
1114}
1115
1116pub fn parse_position_report(
1123 position: &BitmexPosition,
1124 instrument: &InstrumentAny,
1125 ts_init: UnixNanos,
1126) -> anyhow::Result<PositionStatusReport> {
1127 let account_id = bitmex_account_id(position.account);
1128 let instrument_id = instrument.id();
1129 let position_side = parse_position_side(position.current_qty).as_specified();
1130 let quantity = parse_signed_contracts_quantity(position.current_qty.unwrap_or(0), instrument);
1131 let venue_position_id = None; let avg_px_open = position
1133 .avg_entry_price
1134 .and_then(|p| Decimal::from_str(&p.to_string()).ok());
1135 let ts_last = parse_optional_datetime_to_unix_nanos(&position.timestamp, "timestamp");
1136
1137 Ok(PositionStatusReport::new(
1138 account_id,
1139 instrument_id,
1140 position_side,
1141 quantity,
1142 ts_last,
1143 ts_init,
1144 None, venue_position_id, avg_px_open, ))
1148}
1149
1150pub fn get_currency(code: &str) -> Currency {
1155 Currency::get_or_create_crypto(code)
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use std::str::FromStr;
1161
1162 use chrono::{DateTime, Utc};
1163 use nautilus_model::{
1164 data::{BarSpecification, BarType},
1165 enums::{AggregationSource, BarAggregation, LiquiditySide, PositionSide, PriceType},
1166 instruments::InstrumentAny,
1167 };
1168 use rstest::rstest;
1169 use rust_decimal::{Decimal, prelude::ToPrimitive};
1170 use uuid::Uuid;
1171
1172 use super::*;
1173 use crate::{
1174 common::{
1175 enums::{
1176 BitmexContingencyType, BitmexFairMethod, BitmexInstrumentState,
1177 BitmexInstrumentType, BitmexLiquidityIndicator, BitmexMarkMethod,
1178 BitmexOrderStatus, BitmexOrderType, BitmexSide, BitmexTickDirection,
1179 BitmexTimeInForce,
1180 },
1181 testing::load_test_json,
1182 },
1183 http::models::{
1184 BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTradeBin,
1185 BitmexWallet,
1186 },
1187 };
1188
1189 #[rstest]
1190 fn test_perp_instrument_deserialization() {
1191 let json_data = load_test_json("http_get_instrument_xbtusd.json");
1192 let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1193
1194 assert_eq!(instrument.symbol, "XBTUSD");
1195 assert_eq!(instrument.root_symbol, "XBT");
1196 assert_eq!(instrument.state, BitmexInstrumentState::Open);
1197 assert!(instrument.is_inverse);
1198 assert_eq!(instrument.maker_fee, Some(0.0005));
1199 assert_eq!(
1200 instrument.timestamp.to_rfc3339(),
1201 "2024-11-24T23:33:19.034+00:00"
1202 );
1203 }
1204
1205 #[rstest]
1206 fn test_parse_instrument_any_skips_unknown_instrument_state() {
1207 let json_data = load_test_json("http_get_instrument_xbtusd.json");
1208 let mut instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1209 instrument.state = BitmexInstrumentState::Unknown;
1210
1211 let result = parse_instrument_any(&instrument, UnixNanos::default());
1212
1213 assert!(
1214 matches!(result, InstrumentParseResult::Inactive { .. }),
1215 "expected Inactive for unknown state, was {result:?}"
1216 );
1217 }
1218
1219 #[rstest]
1220 fn test_parse_instrument_any_parses_active_crypto_futures_spread() {
1221 let json_data = load_test_json("http_get_instrument_xbtm26_xbtu26_spread.json");
1222 let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1223
1224 let result = parse_instrument_any(&instrument, UnixNanos::default());
1225
1226 match result {
1227 InstrumentParseResult::Ok(instrument_any) => {
1228 let InstrumentAny::CryptoFuturesSpread(spread) = *instrument_any else {
1229 panic!("expected CryptoFuturesSpread variant");
1230 };
1231
1232 assert_eq!(
1233 instrument.instrument_type,
1234 BitmexInstrumentType::FuturesSpread
1235 );
1236 assert_eq!(spread.id.symbol.as_str(), "XBTM26-XBTU26");
1237 assert_eq!(spread.id.venue.as_str(), "BITMEX");
1238 assert_eq!(spread.raw_symbol.as_str(), "XBTM26-XBTU26");
1239 assert_eq!(spread.underlying.code.as_str(), "XBT");
1240 assert_eq!(spread.quote_currency.code.as_str(), "USD");
1241 assert_eq!(spread.settlement_currency.code.as_str(), "XBT");
1242 assert_eq!(spread.strategy_type.as_str(), "FS");
1243 assert!(!spread.is_inverse);
1244 assert_eq!(spread.price_precision, 1);
1245 assert_eq!(spread.size_precision, 0);
1246 assert_eq!(spread.price_increment.as_f64(), 0.5);
1247 assert_eq!(spread.size_increment.as_f64(), 1.0);
1248 assert_eq!(spread.lot_size.as_f64(), 100.0);
1249 assert_eq!(spread.min_quantity.unwrap().as_f64(), 100.0);
1250 assert_eq!(spread.max_quantity.unwrap().as_f64(), 10000000.0);
1251 assert_eq!(spread.min_price.unwrap().as_f64(), -1000000.0);
1252 assert_eq!(spread.max_price.unwrap().as_f64(), 1000000.0);
1253 assert_eq!(spread.maker_fee.to_f64().unwrap(), 0.0005);
1254 assert_eq!(spread.taker_fee.to_f64().unwrap(), 0.0005);
1255 assert!(spread.activation_ns.as_u64() > 0);
1256 assert!(spread.expiration_ns.as_u64() > 0);
1257 }
1258 result => panic!("expected parsed crypto futures spread, was {result:?}"),
1259 }
1260 }
1261
1262 #[rstest]
1263 fn test_parse_orders() {
1264 let json_data = load_test_json("http_get_orders.json");
1265 let orders: Vec<BitmexOrder> = serde_json::from_str(&json_data).unwrap();
1266
1267 assert_eq!(orders.len(), 2);
1268
1269 let order1 = &orders[0];
1271 assert_eq!(order1.symbol, Some(Ustr::from("XBTUSD")));
1272 assert_eq!(order1.side, Some(BitmexSide::Buy));
1273 assert_eq!(order1.order_qty, Some(100));
1274 assert_eq!(order1.price, Some(98000.0));
1275 assert_eq!(order1.ord_status, Some(BitmexOrderStatus::New));
1276 assert_eq!(order1.leaves_qty, Some(100));
1277 assert_eq!(order1.cum_qty, Some(0));
1278
1279 let order2 = &orders[1];
1281 assert_eq!(order2.symbol, Some(Ustr::from("XBTUSD")));
1282 assert_eq!(order2.side, Some(BitmexSide::Sell));
1283 assert_eq!(order2.order_qty, Some(200));
1284 assert_eq!(order2.ord_status, Some(BitmexOrderStatus::Filled));
1285 assert_eq!(order2.leaves_qty, Some(0));
1286 assert_eq!(order2.cum_qty, Some(200));
1287 assert_eq!(order2.avg_px, Some(98950.5));
1288 }
1289
1290 #[rstest]
1291 fn test_parse_executions() {
1292 let json_data = load_test_json("http_get_executions.json");
1293 let executions: Vec<BitmexExecution> = serde_json::from_str(&json_data).unwrap();
1294
1295 assert_eq!(executions.len(), 2);
1296
1297 let exec1 = &executions[0];
1299 assert_eq!(exec1.symbol, Some(Ustr::from("XBTUSD")));
1300 assert_eq!(exec1.side, Some(BitmexSide::Sell));
1301 assert_eq!(exec1.last_qty, 100);
1302 assert_eq!(exec1.last_px, 98950.0);
1303 assert_eq!(
1304 exec1.last_liquidity_ind,
1305 Some(BitmexLiquidityIndicator::Maker)
1306 );
1307 assert_eq!(exec1.commission, Some(0.00075));
1308
1309 let exec2 = &executions[1];
1311 assert_eq!(
1312 exec2.last_liquidity_ind,
1313 Some(BitmexLiquidityIndicator::Taker)
1314 );
1315 assert_eq!(exec2.last_px, 98951.0);
1316 }
1317
1318 #[rstest]
1319 fn test_parse_positions() {
1320 let json_data = load_test_json("http_get_positions.json");
1321 let positions: Vec<BitmexPosition> = serde_json::from_str(&json_data).unwrap();
1322
1323 assert_eq!(positions.len(), 1);
1324
1325 let position = &positions[0];
1326 assert_eq!(position.account, 1234567);
1327 assert_eq!(position.symbol, "XBTUSD");
1328 assert_eq!(position.current_qty, Some(100));
1329 assert_eq!(position.avg_entry_price, Some(98390.88));
1330 assert_eq!(position.unrealised_pnl, Some(1350));
1331 assert_eq!(position.realised_pnl, Some(-227));
1332 assert_eq!(position.is_open, Some(true));
1333 }
1334
1335 #[rstest]
1336 fn test_parse_trades() {
1337 let json_data = load_test_json("http_get_trades.json");
1338 let trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
1339
1340 assert_eq!(trades.len(), 3);
1341
1342 let trade1 = &trades[0];
1344 assert_eq!(trade1.symbol, "XBTUSD");
1345 assert_eq!(trade1.side, Some(BitmexSide::Buy));
1346 assert_eq!(trade1.size, 100);
1347 assert_eq!(trade1.price, 98950.0);
1348
1349 let trade3 = &trades[2];
1351 assert_eq!(trade3.side, Some(BitmexSide::Sell));
1352 assert_eq!(trade3.size, 50);
1353 assert_eq!(trade3.price, 98949.5);
1354 }
1355
1356 #[rstest]
1357 fn test_parse_trade_derives_trade_id_when_trd_match_id_missing() {
1358 let json_data = load_test_json("http_get_trades.json");
1359 let mut trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
1360 trades[0].trd_match_id = None;
1361 trades[1] = trades[0].clone();
1362 trades[2] = trades[0].clone();
1363 trades[2].price += 1.0;
1364
1365 let instrument =
1366 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1367 .unwrap();
1368
1369 let tick_a = parse_trade(&trades[0], &instrument, UnixNanos::from(1)).unwrap();
1370 let tick_b = parse_trade(&trades[1], &instrument, UnixNanos::from(1)).unwrap();
1371 let tick_c = parse_trade(&trades[2], &instrument, UnixNanos::from(1)).unwrap();
1372
1373 assert_eq!(
1374 tick_a.trade_id, tick_b.trade_id,
1375 "derivation must be stable"
1376 );
1377 assert_eq!(tick_a.trade_id.as_str().len(), 16);
1378 assert_ne!(
1379 tick_a.trade_id, tick_c.trade_id,
1380 "distinct price must distinguish"
1381 );
1382 }
1383
1384 #[rstest]
1385 fn test_parse_wallet() {
1386 let json_data = load_test_json("http_get_wallet.json");
1387 let wallets: Vec<BitmexWallet> = serde_json::from_str(&json_data).unwrap();
1388
1389 assert_eq!(wallets.len(), 1);
1390
1391 let wallet = &wallets[0];
1392 assert_eq!(wallet.account, 1234567);
1393 assert_eq!(wallet.currency, "XBt");
1394 assert_eq!(wallet.amount, Some(1000123456));
1395 assert_eq!(wallet.delta_amount, Some(123456));
1396 }
1397
1398 #[rstest]
1399 fn test_parse_trade_bins() {
1400 let json_data = load_test_json("http_get_trade_bins.json");
1401 let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1402
1403 assert_eq!(bins.len(), 3);
1404
1405 let bin1 = &bins[0];
1407 assert_eq!(bin1.symbol, "XBTUSD");
1408 assert_eq!(bin1.open, Some(98900.0));
1409 assert_eq!(bin1.high, Some(98980.5));
1410 assert_eq!(bin1.low, Some(98890.0));
1411 assert_eq!(bin1.close, Some(98950.0));
1412 assert_eq!(bin1.volume, Some(150000));
1413 assert_eq!(bin1.trades, Some(45));
1414
1415 let bin3 = &bins[2];
1417 assert_eq!(bin3.close, Some(98970.0));
1418 assert_eq!(bin3.volume, Some(78000));
1419 }
1420
1421 #[rstest]
1422 fn test_parse_trade_bin_to_bar() {
1423 let json_data = load_test_json("http_get_trade_bins.json");
1424 let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1425 let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1426 let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1427
1428 let ts_init = UnixNanos::from(1u64);
1429 let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1430 InstrumentParseResult::Ok(inst) => inst,
1431 other => panic!("Expected Ok, was {other:?}"),
1432 };
1433
1434 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1435 let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1436
1437 let bar = parse_trade_bin(&bins[0], &instrument_any, &bar_type, ts_init).unwrap();
1438
1439 let precision = instrument_any.price_precision();
1440 let expected_open =
1441 Price::from_decimal_dp(Decimal::from_str("98900.0").unwrap(), precision)
1442 .expect("open price");
1443 let expected_close =
1444 Price::from_decimal_dp(Decimal::from_str("98950.0").unwrap(), precision)
1445 .expect("close price");
1446
1447 assert_eq!(bar.bar_type, bar_type);
1448 assert_eq!(bar.open, expected_open);
1449 assert_eq!(bar.close, expected_close);
1450 }
1451
1452 #[rstest]
1453 fn test_parse_trade_bin_extreme_adjustment() {
1454 let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1455 let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1456
1457 let ts_init = UnixNanos::from(1u64);
1458 let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1459 InstrumentParseResult::Ok(inst) => inst,
1460 other => panic!("Expected Ok, was {other:?}"),
1461 };
1462
1463 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1464 let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1465
1466 let bin = BitmexTradeBin {
1467 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1468 .unwrap()
1469 .with_timezone(&Utc),
1470 symbol: Ustr::from("XBTUSD"),
1471 open: Some(50_000.0),
1472 high: Some(49_990.0),
1473 low: Some(50_010.0),
1474 close: Some(50_005.0),
1475 trades: Some(5),
1476 volume: Some(1_000),
1477 vwap: None,
1478 last_size: None,
1479 turnover: None,
1480 home_notional: None,
1481 foreign_notional: None,
1482 };
1483
1484 let bar = parse_trade_bin(&bin, &instrument_any, &bar_type, ts_init).unwrap();
1485
1486 let precision = instrument_any.price_precision();
1487 let expected_high =
1488 Price::from_decimal_dp(Decimal::from_str("50010.0").unwrap(), precision)
1489 .expect("high price");
1490 let expected_low = Price::from_decimal_dp(Decimal::from_str("49990.0").unwrap(), precision)
1491 .expect("low price");
1492 let expected_open =
1493 Price::from_decimal_dp(Decimal::from_str("50000.0").unwrap(), precision)
1494 .expect("open price");
1495
1496 assert_eq!(bar.high, expected_high);
1497 assert_eq!(bar.low, expected_low);
1498 assert_eq!(bar.open, expected_open);
1499 }
1500
1501 #[rstest]
1502 fn test_parse_order_status_report() {
1503 let order = BitmexOrder {
1504 account: 123456,
1505 symbol: Some(Ustr::from("XBTUSD")),
1506 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
1507 cl_ord_id: Some(Ustr::from("client-123")),
1508 cl_ord_link_id: None,
1509 side: Some(BitmexSide::Buy),
1510 ord_type: Some(BitmexOrderType::Limit),
1511 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1512 ord_status: Some(BitmexOrderStatus::New),
1513 order_qty: Some(100),
1514 cum_qty: Some(50),
1515 price: Some(50000.0),
1516 stop_px: Some(49000.0),
1517 display_qty: None,
1518 peg_offset_value: None,
1519 peg_price_type: None,
1520 currency: Some(Ustr::from("USD")),
1521 settl_currency: Some(Ustr::from("XBt")),
1522 exec_inst: Some(vec![
1523 BitmexExecInstruction::ParticipateDoNotInitiate,
1524 BitmexExecInstruction::ReduceOnly,
1525 ]),
1526 contingency_type: Some(BitmexContingencyType::OneCancelsTheOther),
1527 ex_destination: None,
1528 triggered: None,
1529 working_indicator: Some(true),
1530 ord_rej_reason: None,
1531 leaves_qty: Some(50),
1532 avg_px: None,
1533 multi_leg_reporting_type: None,
1534 text: None,
1535 transact_time: Some(
1536 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1537 .unwrap()
1538 .with_timezone(&Utc),
1539 ),
1540 timestamp: Some(
1541 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1542 .unwrap()
1543 .with_timezone(&Utc),
1544 ),
1545 };
1546
1547 let instrument =
1548 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1549 .unwrap();
1550 let report =
1551 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1552 .unwrap();
1553
1554 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
1555 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1556 assert_eq!(
1557 report.venue_order_id.as_str(),
1558 "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
1559 );
1560 assert_eq!(report.client_order_id.unwrap().as_str(), "client-123");
1561 assert_eq!(report.quantity.as_f64(), 100.0);
1562 assert_eq!(report.filled_qty.as_f64(), 50.0);
1563 assert_eq!(report.price.unwrap().as_f64(), 50000.0);
1564 assert_eq!(report.trigger_price.unwrap().as_f64(), 49000.0);
1565 assert!(report.post_only);
1566 assert!(report.reduce_only);
1567 }
1568
1569 #[rstest]
1570 fn test_parse_order_status_report_minimal() {
1571 let order = BitmexOrder {
1572 account: 0, symbol: Some(Ustr::from("ETHUSD")),
1574 order_id: Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap(),
1575 cl_ord_id: None,
1576 cl_ord_link_id: None,
1577 side: Some(BitmexSide::Sell),
1578 ord_type: Some(BitmexOrderType::Market),
1579 time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1580 ord_status: Some(BitmexOrderStatus::Filled),
1581 order_qty: Some(200),
1582 cum_qty: Some(200),
1583 price: None,
1584 stop_px: None,
1585 display_qty: None,
1586 peg_offset_value: None,
1587 peg_price_type: None,
1588 currency: None,
1589 settl_currency: None,
1590 exec_inst: None,
1591 contingency_type: None,
1592 ex_destination: None,
1593 triggered: None,
1594 working_indicator: Some(false),
1595 ord_rej_reason: None,
1596 leaves_qty: Some(0),
1597 avg_px: None,
1598 multi_leg_reporting_type: None,
1599 text: None,
1600 transact_time: Some(
1601 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1602 .unwrap()
1603 .with_timezone(&Utc),
1604 ),
1605 timestamp: Some(
1606 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1607 .unwrap()
1608 .with_timezone(&Utc),
1609 ),
1610 };
1611
1612 let mut instrument_def = create_test_perpetual_instrument();
1613 instrument_def.symbol = Ustr::from("ETHUSD");
1614 instrument_def.underlying = Ustr::from("ETH");
1615 instrument_def.quote_currency = Ustr::from("USD");
1616 instrument_def.settl_currency = Some(Ustr::from("USDt"));
1617 let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
1618 let report =
1619 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1620 .unwrap();
1621
1622 assert_eq!(report.account_id.to_string(), "BITMEX-0");
1623 assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
1624 assert_eq!(
1625 report.venue_order_id.as_str(),
1626 "11111111-2222-3333-4444-555555555555"
1627 );
1628 assert!(report.client_order_id.is_none());
1629 assert_eq!(report.quantity.as_f64(), 200.0);
1630 assert_eq!(report.filled_qty.as_f64(), 200.0);
1631 assert!(report.price.is_none());
1632 assert!(report.trigger_price.is_none());
1633 assert!(!report.post_only);
1634 assert!(!report.reduce_only);
1635 }
1636
1637 #[rstest]
1638 fn test_parse_order_status_report_missing_order_qty_reconstructed() {
1639 let order = BitmexOrder {
1640 account: 789012,
1641 symbol: Some(Ustr::from("XBTUSD")),
1642 order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1643 cl_ord_id: Some(Ustr::from("client-cancel-test")),
1644 cl_ord_link_id: None,
1645 side: Some(BitmexSide::Buy),
1646 ord_type: Some(BitmexOrderType::Limit),
1647 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1648 ord_status: Some(BitmexOrderStatus::Canceled),
1649 order_qty: None, cum_qty: Some(75), leaves_qty: Some(25), price: Some(45000.0),
1653 stop_px: None,
1654 display_qty: None,
1655 peg_offset_value: None,
1656 peg_price_type: None,
1657 currency: Some(Ustr::from("USD")),
1658 settl_currency: Some(Ustr::from("XBt")),
1659 exec_inst: None,
1660 contingency_type: None,
1661 ex_destination: None,
1662 triggered: None,
1663 working_indicator: Some(false),
1664 ord_rej_reason: None,
1665 avg_px: Some(45050.0),
1666 multi_leg_reporting_type: None,
1667 text: None,
1668 transact_time: Some(
1669 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1670 .unwrap()
1671 .with_timezone(&Utc),
1672 ),
1673 timestamp: Some(
1674 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1675 .unwrap()
1676 .with_timezone(&Utc),
1677 ),
1678 };
1679
1680 let instrument =
1681 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1682 .unwrap();
1683 let report =
1684 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1685 .unwrap();
1686
1687 assert_eq!(report.quantity.as_f64(), 100.0); assert_eq!(report.filled_qty.as_f64(), 75.0);
1690 assert_eq!(report.order_status, OrderStatus::Canceled);
1691 }
1692
1693 #[rstest]
1694 fn test_parse_order_status_report_uses_provided_order_qty() {
1695 let order = BitmexOrder {
1696 account: 123456,
1697 symbol: Some(Ustr::from("XBTUSD")),
1698 order_id: Uuid::parse_str("bbbbcccc-dddd-eeee-ffff-000000000000").unwrap(),
1699 cl_ord_id: Some(Ustr::from("client-provided-qty")),
1700 cl_ord_link_id: None,
1701 side: Some(BitmexSide::Sell),
1702 ord_type: Some(BitmexOrderType::Limit),
1703 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1704 ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1705 order_qty: Some(150), cum_qty: Some(50), leaves_qty: Some(100), price: Some(48000.0),
1709 stop_px: None,
1710 display_qty: None,
1711 peg_offset_value: None,
1712 peg_price_type: None,
1713 currency: Some(Ustr::from("USD")),
1714 settl_currency: Some(Ustr::from("XBt")),
1715 exec_inst: None,
1716 contingency_type: None,
1717 ex_destination: None,
1718 triggered: None,
1719 working_indicator: Some(true),
1720 ord_rej_reason: None,
1721 avg_px: Some(48100.0),
1722 multi_leg_reporting_type: None,
1723 text: None,
1724 transact_time: Some(
1725 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1726 .unwrap()
1727 .with_timezone(&Utc),
1728 ),
1729 timestamp: Some(
1730 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1731 .unwrap()
1732 .with_timezone(&Utc),
1733 ),
1734 };
1735
1736 let instrument =
1737 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1738 .unwrap();
1739 let report =
1740 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1741 .unwrap();
1742
1743 assert_eq!(report.quantity.as_f64(), 150.0);
1745 assert_eq!(report.filled_qty.as_f64(), 50.0);
1746 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
1747 }
1748
1749 #[rstest]
1750 fn test_parse_order_status_report_missing_order_qty_fails() {
1751 let order = BitmexOrder {
1752 account: 789012,
1753 symbol: Some(Ustr::from("XBTUSD")),
1754 order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1755 cl_ord_id: Some(Ustr::from("client-fail-test")),
1756 cl_ord_link_id: None,
1757 side: Some(BitmexSide::Buy),
1758 ord_type: Some(BitmexOrderType::Limit),
1759 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1760 ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1761 order_qty: None, cum_qty: Some(75), leaves_qty: None, price: Some(45000.0),
1765 stop_px: None,
1766 display_qty: None,
1767 peg_offset_value: None,
1768 peg_price_type: None,
1769 currency: Some(Ustr::from("USD")),
1770 settl_currency: Some(Ustr::from("XBt")),
1771 exec_inst: None,
1772 contingency_type: None,
1773 ex_destination: None,
1774 triggered: None,
1775 working_indicator: Some(false),
1776 ord_rej_reason: None,
1777 avg_px: None,
1778 multi_leg_reporting_type: None,
1779 text: None,
1780 transact_time: Some(
1781 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1782 .unwrap()
1783 .with_timezone(&Utc),
1784 ),
1785 timestamp: Some(
1786 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1787 .unwrap()
1788 .with_timezone(&Utc),
1789 ),
1790 };
1791
1792 let instrument =
1793 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1794 .unwrap();
1795
1796 let result =
1798 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
1799 assert!(result.is_err());
1800 assert!(
1801 result
1802 .unwrap_err()
1803 .to_string()
1804 .contains("Order missing order_qty and cannot reconstruct")
1805 );
1806 }
1807
1808 #[rstest]
1809 fn test_parse_order_status_report_canceled_missing_all_quantities() {
1810 let order = BitmexOrder {
1811 account: 123456,
1812 symbol: Some(Ustr::from("XBTUSD")),
1813 order_id: Uuid::parse_str("ffff0000-1111-2222-3333-444444444444").unwrap(),
1814 cl_ord_id: Some(Ustr::from("client-cancel-no-qty")),
1815 cl_ord_link_id: None,
1816 side: Some(BitmexSide::Buy),
1817 ord_type: Some(BitmexOrderType::Limit),
1818 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1819 ord_status: Some(BitmexOrderStatus::Canceled),
1820 order_qty: None, cum_qty: None, leaves_qty: None, price: Some(50000.0),
1824 stop_px: None,
1825 display_qty: None,
1826 peg_offset_value: None,
1827 peg_price_type: None,
1828 currency: Some(Ustr::from("USD")),
1829 settl_currency: Some(Ustr::from("XBt")),
1830 exec_inst: None,
1831 contingency_type: None,
1832 ex_destination: None,
1833 triggered: None,
1834 working_indicator: Some(false),
1835 ord_rej_reason: None,
1836 avg_px: None,
1837 multi_leg_reporting_type: None,
1838 text: None,
1839 transact_time: Some(
1840 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1841 .unwrap()
1842 .with_timezone(&Utc),
1843 ),
1844 timestamp: Some(
1845 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1846 .unwrap()
1847 .with_timezone(&Utc),
1848 ),
1849 };
1850
1851 let instrument =
1852 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1853 .unwrap();
1854 let report =
1855 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1856 .unwrap();
1857
1858 assert_eq!(report.order_status, OrderStatus::Canceled);
1860 assert_eq!(report.quantity.as_f64(), 0.0);
1861 assert_eq!(report.filled_qty.as_f64(), 0.0);
1862 }
1863
1864 #[rstest]
1865 fn test_parse_order_status_report_rejected_with_reason() {
1866 let order = BitmexOrder {
1867 account: 123456,
1868 symbol: Some(Ustr::from("XBTUSD")),
1869 order_id: Uuid::parse_str("ccccdddd-eeee-ffff-0000-111111111111").unwrap(),
1870 cl_ord_id: Some(Ustr::from("client-rejected")),
1871 cl_ord_link_id: None,
1872 side: Some(BitmexSide::Buy),
1873 ord_type: Some(BitmexOrderType::Limit),
1874 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1875 ord_status: Some(BitmexOrderStatus::Rejected),
1876 order_qty: Some(100),
1877 cum_qty: Some(0),
1878 leaves_qty: Some(0),
1879 price: Some(50000.0),
1880 stop_px: None,
1881 display_qty: None,
1882 peg_offset_value: None,
1883 peg_price_type: None,
1884 currency: Some(Ustr::from("USD")),
1885 settl_currency: Some(Ustr::from("XBt")),
1886 exec_inst: None,
1887 contingency_type: None,
1888 ex_destination: None,
1889 triggered: None,
1890 working_indicator: Some(false),
1891 ord_rej_reason: Some(Ustr::from("Insufficient margin")),
1892 avg_px: None,
1893 multi_leg_reporting_type: None,
1894 text: None,
1895 transact_time: Some(
1896 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1897 .unwrap()
1898 .with_timezone(&Utc),
1899 ),
1900 timestamp: Some(
1901 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1902 .unwrap()
1903 .with_timezone(&Utc),
1904 ),
1905 };
1906
1907 let instrument =
1908 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1909 .unwrap();
1910 let report =
1911 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1912 .unwrap();
1913
1914 assert_eq!(report.order_status, OrderStatus::Rejected);
1915 assert_eq!(
1916 report.cancel_reason,
1917 Some("Insufficient margin".to_string())
1918 );
1919 }
1920
1921 #[rstest]
1922 fn test_parse_order_status_report_rejected_with_text_fallback() {
1923 let order = BitmexOrder {
1924 account: 123456,
1925 symbol: Some(Ustr::from("XBTUSD")),
1926 order_id: Uuid::parse_str("ddddeeee-ffff-0000-1111-222222222222").unwrap(),
1927 cl_ord_id: Some(Ustr::from("client-rejected-text")),
1928 cl_ord_link_id: None,
1929 side: Some(BitmexSide::Sell),
1930 ord_type: Some(BitmexOrderType::Limit),
1931 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1932 ord_status: Some(BitmexOrderStatus::Rejected),
1933 order_qty: Some(100),
1934 cum_qty: Some(0),
1935 leaves_qty: Some(0),
1936 price: Some(50000.0),
1937 stop_px: None,
1938 display_qty: None,
1939 peg_offset_value: None,
1940 peg_price_type: None,
1941 currency: Some(Ustr::from("USD")),
1942 settl_currency: Some(Ustr::from("XBt")),
1943 exec_inst: None,
1944 contingency_type: None,
1945 ex_destination: None,
1946 triggered: None,
1947 working_indicator: Some(false),
1948 ord_rej_reason: None,
1949 avg_px: None,
1950 multi_leg_reporting_type: None,
1951 text: Some(Ustr::from("Order would immediately execute")),
1952 transact_time: Some(
1953 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1954 .unwrap()
1955 .with_timezone(&Utc),
1956 ),
1957 timestamp: Some(
1958 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1959 .unwrap()
1960 .with_timezone(&Utc),
1961 ),
1962 };
1963
1964 let instrument =
1965 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1966 .unwrap();
1967 let report =
1968 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1969 .unwrap();
1970
1971 assert_eq!(report.order_status, OrderStatus::Rejected);
1972 assert_eq!(
1973 report.cancel_reason,
1974 Some("Order would immediately execute".to_string())
1975 );
1976 }
1977
1978 #[rstest]
1979 fn test_parse_order_status_report_rejected_without_reason() {
1980 let order = BitmexOrder {
1981 account: 123456,
1982 symbol: Some(Ustr::from("XBTUSD")),
1983 order_id: Uuid::parse_str("eeeeffff-0000-1111-2222-333333333333").unwrap(),
1984 cl_ord_id: Some(Ustr::from("client-rejected-no-reason")),
1985 cl_ord_link_id: None,
1986 side: Some(BitmexSide::Buy),
1987 ord_type: Some(BitmexOrderType::Market),
1988 time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1989 ord_status: Some(BitmexOrderStatus::Rejected),
1990 order_qty: Some(50),
1991 cum_qty: Some(0),
1992 leaves_qty: Some(0),
1993 price: None,
1994 stop_px: None,
1995 display_qty: None,
1996 peg_offset_value: None,
1997 peg_price_type: None,
1998 currency: Some(Ustr::from("USD")),
1999 settl_currency: Some(Ustr::from("XBt")),
2000 exec_inst: None,
2001 contingency_type: None,
2002 ex_destination: None,
2003 triggered: None,
2004 working_indicator: Some(false),
2005 ord_rej_reason: None,
2006 avg_px: None,
2007 multi_leg_reporting_type: None,
2008 text: None,
2009 transact_time: Some(
2010 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2011 .unwrap()
2012 .with_timezone(&Utc),
2013 ),
2014 timestamp: Some(
2015 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
2016 .unwrap()
2017 .with_timezone(&Utc),
2018 ),
2019 };
2020
2021 let instrument =
2022 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2023 .unwrap();
2024 let report =
2025 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
2026 .unwrap();
2027
2028 assert_eq!(report.order_status, OrderStatus::Rejected);
2029 assert_eq!(report.cancel_reason, None);
2030 }
2031
2032 #[rstest]
2033 fn test_parse_fill_report() {
2034 let exec = BitmexExecution {
2035 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
2036 account: 654321,
2037 symbol: Some(Ustr::from("XBTUSD")),
2038 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
2039 cl_ord_id: Some(Ustr::from("client-456")),
2040 side: Some(BitmexSide::Buy),
2041 last_qty: 50,
2042 last_px: 50100.5,
2043 commission: Some(0.00075),
2044 settl_currency: Some(Ustr::from("XBt")),
2045 last_liquidity_ind: Some(BitmexLiquidityIndicator::Taker),
2046 trd_match_id: Some(Uuid::parse_str("99999999-8888-7777-6666-555555555555").unwrap()),
2047 transact_time: Some(
2048 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2049 .unwrap()
2050 .with_timezone(&Utc),
2051 ),
2052 cl_ord_link_id: None,
2053 underlying_last_px: None,
2054 last_mkt: None,
2055 order_qty: Some(50),
2056 price: Some(50100.0),
2057 display_qty: None,
2058 stop_px: None,
2059 peg_offset_value: None,
2060 peg_price_type: None,
2061 currency: None,
2062 exec_type: BitmexExecType::Trade,
2063 ord_type: BitmexOrderType::Limit,
2064 time_in_force: BitmexTimeInForce::GoodTillCancel,
2065 exec_inst: None,
2066 contingency_type: None,
2067 ex_destination: None,
2068 ord_status: Some(BitmexOrderStatus::Filled),
2069 triggered: None,
2070 working_indicator: None,
2071 ord_rej_reason: None,
2072 leaves_qty: None,
2073 cum_qty: Some(50),
2074 avg_px: Some(50100.5),
2075 trade_publish_indicator: None,
2076 multi_leg_reporting_type: None,
2077 text: None,
2078 exec_cost: None,
2079 exec_comm: None,
2080 home_notional: None,
2081 foreign_notional: None,
2082 timestamp: None,
2083 };
2084
2085 let instrument =
2086 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2087 .unwrap();
2088
2089 let report = parse_fill_report(&exec, &instrument, UnixNanos::from(1)).unwrap();
2090
2091 assert_eq!(report.account_id.to_string(), "BITMEX-654321");
2092 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
2093 assert_eq!(
2094 report.venue_order_id.as_str(),
2095 "a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6"
2096 );
2097 assert_eq!(
2098 report.trade_id.to_string(),
2099 "99999999-8888-7777-6666-555555555555"
2100 );
2101 assert_eq!(report.client_order_id.unwrap().as_str(), "client-456");
2102 assert_eq!(report.last_qty.as_f64(), 50.0);
2103 assert_eq!(report.last_px.as_f64(), 50100.5);
2104 assert_eq!(report.commission.as_f64(), 0.00075);
2105 assert_eq!(report.commission.currency.code.as_str(), "XBT");
2106 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
2107 }
2108
2109 #[rstest]
2110 fn test_parse_fill_report_with_missing_trd_match_id() {
2111 let exec = BitmexExecution {
2112 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
2113 account: 111111,
2114 symbol: Some(Ustr::from("ETHUSD")),
2115 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
2116 cl_ord_id: None,
2117 side: Some(BitmexSide::Sell),
2118 last_qty: 100,
2119 last_px: 3000.0,
2120 commission: None,
2121 settl_currency: None,
2122 last_liquidity_ind: Some(BitmexLiquidityIndicator::Maker),
2123 trd_match_id: None, transact_time: Some(
2125 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2126 .unwrap()
2127 .with_timezone(&Utc),
2128 ),
2129 cl_ord_link_id: None,
2130 underlying_last_px: None,
2131 last_mkt: None,
2132 order_qty: Some(100),
2133 price: Some(3000.0),
2134 display_qty: None,
2135 stop_px: None,
2136 peg_offset_value: None,
2137 peg_price_type: None,
2138 currency: None,
2139 exec_type: BitmexExecType::Trade,
2140 ord_type: BitmexOrderType::Market,
2141 time_in_force: BitmexTimeInForce::ImmediateOrCancel,
2142 exec_inst: None,
2143 contingency_type: None,
2144 ex_destination: None,
2145 ord_status: Some(BitmexOrderStatus::Filled),
2146 triggered: None,
2147 working_indicator: None,
2148 ord_rej_reason: None,
2149 leaves_qty: None,
2150 cum_qty: Some(100),
2151 avg_px: Some(3000.0),
2152 trade_publish_indicator: None,
2153 multi_leg_reporting_type: None,
2154 text: None,
2155 exec_cost: None,
2156 exec_comm: None,
2157 home_notional: None,
2158 foreign_notional: None,
2159 timestamp: None,
2160 };
2161
2162 let mut instrument_def = create_test_perpetual_instrument();
2163 instrument_def.symbol = Ustr::from("ETHUSD");
2164 instrument_def.underlying = Ustr::from("ETH");
2165 instrument_def.quote_currency = Ustr::from("USD");
2166 instrument_def.settl_currency = Some(Ustr::from("USDt"));
2167 let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
2168
2169 let report = parse_fill_report(&exec, &instrument, UnixNanos::from(1)).unwrap();
2170
2171 assert_eq!(report.account_id.to_string(), "BITMEX-111111");
2172 assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
2173 assert_eq!(
2174 report.trade_id.to_string(),
2175 "f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6"
2176 );
2177 assert!(report.client_order_id.is_none());
2178 assert_eq!(report.commission.as_f64(), 0.0);
2179 assert_eq!(report.commission.currency.code.as_str(), "XBT");
2180 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
2181 }
2182
2183 #[rstest]
2184 fn test_parse_position_report() {
2185 let position = BitmexPosition {
2186 account: 789012,
2187 symbol: Ustr::from("XBTUSD"),
2188 current_qty: Some(1000),
2189 timestamp: Some(
2190 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2191 .unwrap()
2192 .with_timezone(&Utc),
2193 ),
2194 currency: None,
2195 underlying: None,
2196 quote_currency: None,
2197 commission: None,
2198 init_margin_req: None,
2199 maint_margin_req: None,
2200 risk_limit: None,
2201 leverage: None,
2202 cross_margin: None,
2203 deleverage_percentile: None,
2204 rebalanced_pnl: None,
2205 prev_realised_pnl: None,
2206 prev_unrealised_pnl: None,
2207 prev_close_price: None,
2208 opening_timestamp: None,
2209 opening_qty: None,
2210 opening_cost: None,
2211 opening_comm: None,
2212 open_order_buy_qty: None,
2213 open_order_buy_cost: None,
2214 open_order_buy_premium: None,
2215 open_order_sell_qty: None,
2216 open_order_sell_cost: None,
2217 open_order_sell_premium: None,
2218 exec_buy_qty: None,
2219 exec_buy_cost: None,
2220 exec_sell_qty: None,
2221 exec_sell_cost: None,
2222 exec_qty: None,
2223 exec_cost: None,
2224 exec_comm: None,
2225 current_timestamp: None,
2226 current_cost: None,
2227 current_comm: None,
2228 realised_cost: None,
2229 unrealised_cost: None,
2230 gross_open_cost: None,
2231 gross_open_premium: None,
2232 gross_exec_cost: None,
2233 is_open: Some(true),
2234 mark_price: None,
2235 mark_value: None,
2236 risk_value: None,
2237 home_notional: None,
2238 foreign_notional: None,
2239 pos_state: None,
2240 pos_cost: None,
2241 pos_cost2: None,
2242 pos_cross: None,
2243 pos_init: None,
2244 pos_comm: None,
2245 pos_loss: None,
2246 pos_margin: None,
2247 pos_maint: None,
2248 pos_allowance: None,
2249 taxable_margin: None,
2250 init_margin: None,
2251 maint_margin: None,
2252 session_margin: None,
2253 target_excess_margin: None,
2254 var_margin: None,
2255 realised_gross_pnl: None,
2256 realised_tax: None,
2257 realised_pnl: None,
2258 unrealised_gross_pnl: None,
2259 long_bankrupt: None,
2260 short_bankrupt: None,
2261 tax_base: None,
2262 indicative_tax_rate: None,
2263 indicative_tax: None,
2264 unrealised_tax: None,
2265 unrealised_pnl: None,
2266 unrealised_pnl_pcnt: None,
2267 unrealised_roe_pcnt: None,
2268 avg_cost_price: None,
2269 avg_entry_price: None,
2270 break_even_price: None,
2271 margin_call_price: None,
2272 liquidation_price: None,
2273 bankrupt_price: None,
2274 last_price: None,
2275 last_value: None,
2276 };
2277
2278 let instrument =
2279 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2280 .unwrap();
2281
2282 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2283
2284 assert_eq!(report.account_id.to_string(), "BITMEX-789012");
2285 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
2286 assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
2287 assert_eq!(report.quantity.as_f64(), 1000.0);
2288 }
2289
2290 #[rstest]
2291 fn test_parse_position_report_short() {
2292 let position = BitmexPosition {
2293 account: 789012,
2294 symbol: Ustr::from("ETHUSD"),
2295 current_qty: Some(-500),
2296 timestamp: Some(
2297 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2298 .unwrap()
2299 .with_timezone(&Utc),
2300 ),
2301 currency: None,
2302 underlying: None,
2303 quote_currency: None,
2304 commission: None,
2305 init_margin_req: None,
2306 maint_margin_req: None,
2307 risk_limit: None,
2308 leverage: None,
2309 cross_margin: None,
2310 deleverage_percentile: None,
2311 rebalanced_pnl: None,
2312 prev_realised_pnl: None,
2313 prev_unrealised_pnl: None,
2314 prev_close_price: None,
2315 opening_timestamp: None,
2316 opening_qty: None,
2317 opening_cost: None,
2318 opening_comm: None,
2319 open_order_buy_qty: None,
2320 open_order_buy_cost: None,
2321 open_order_buy_premium: None,
2322 open_order_sell_qty: None,
2323 open_order_sell_cost: None,
2324 open_order_sell_premium: None,
2325 exec_buy_qty: None,
2326 exec_buy_cost: None,
2327 exec_sell_qty: None,
2328 exec_sell_cost: None,
2329 exec_qty: None,
2330 exec_cost: None,
2331 exec_comm: None,
2332 current_timestamp: None,
2333 current_cost: None,
2334 current_comm: None,
2335 realised_cost: None,
2336 unrealised_cost: None,
2337 gross_open_cost: None,
2338 gross_open_premium: None,
2339 gross_exec_cost: None,
2340 is_open: Some(true),
2341 mark_price: None,
2342 mark_value: None,
2343 risk_value: None,
2344 home_notional: None,
2345 foreign_notional: None,
2346 pos_state: None,
2347 pos_cost: None,
2348 pos_cost2: None,
2349 pos_cross: None,
2350 pos_init: None,
2351 pos_comm: None,
2352 pos_loss: None,
2353 pos_margin: None,
2354 pos_maint: None,
2355 pos_allowance: None,
2356 taxable_margin: None,
2357 init_margin: None,
2358 maint_margin: None,
2359 session_margin: None,
2360 target_excess_margin: None,
2361 var_margin: None,
2362 realised_gross_pnl: None,
2363 realised_tax: None,
2364 realised_pnl: None,
2365 unrealised_gross_pnl: None,
2366 long_bankrupt: None,
2367 short_bankrupt: None,
2368 tax_base: None,
2369 indicative_tax_rate: None,
2370 indicative_tax: None,
2371 unrealised_tax: None,
2372 unrealised_pnl: None,
2373 unrealised_pnl_pcnt: None,
2374 unrealised_roe_pcnt: None,
2375 avg_cost_price: None,
2376 avg_entry_price: None,
2377 break_even_price: None,
2378 margin_call_price: None,
2379 liquidation_price: None,
2380 bankrupt_price: None,
2381 last_price: None,
2382 last_value: None,
2383 };
2384
2385 let mut instrument_def = create_test_futures_instrument();
2386 instrument_def.symbol = Ustr::from("ETHUSD");
2387 instrument_def.underlying = Ustr::from("ETH");
2388 instrument_def.quote_currency = Ustr::from("USD");
2389 instrument_def.settl_currency = Some(Ustr::from("USD"));
2390 let instrument = parse_futures_instrument(&instrument_def, UnixNanos::default()).unwrap();
2391
2392 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2393
2394 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
2395 assert_eq!(report.quantity.as_f64(), 500.0); }
2397
2398 #[rstest]
2399 fn test_parse_position_report_flat() {
2400 let position = BitmexPosition {
2401 account: 789012,
2402 symbol: Ustr::from("SOLUSD"),
2403 current_qty: Some(0),
2404 timestamp: Some(
2405 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2406 .unwrap()
2407 .with_timezone(&Utc),
2408 ),
2409 currency: None,
2410 underlying: None,
2411 quote_currency: None,
2412 commission: None,
2413 init_margin_req: None,
2414 maint_margin_req: None,
2415 risk_limit: None,
2416 leverage: None,
2417 cross_margin: None,
2418 deleverage_percentile: None,
2419 rebalanced_pnl: None,
2420 prev_realised_pnl: None,
2421 prev_unrealised_pnl: None,
2422 prev_close_price: None,
2423 opening_timestamp: None,
2424 opening_qty: None,
2425 opening_cost: None,
2426 opening_comm: None,
2427 open_order_buy_qty: None,
2428 open_order_buy_cost: None,
2429 open_order_buy_premium: None,
2430 open_order_sell_qty: None,
2431 open_order_sell_cost: None,
2432 open_order_sell_premium: None,
2433 exec_buy_qty: None,
2434 exec_buy_cost: None,
2435 exec_sell_qty: None,
2436 exec_sell_cost: None,
2437 exec_qty: None,
2438 exec_cost: None,
2439 exec_comm: None,
2440 current_timestamp: None,
2441 current_cost: None,
2442 current_comm: None,
2443 realised_cost: None,
2444 unrealised_cost: None,
2445 gross_open_cost: None,
2446 gross_open_premium: None,
2447 gross_exec_cost: None,
2448 is_open: Some(true),
2449 mark_price: None,
2450 mark_value: None,
2451 risk_value: None,
2452 home_notional: None,
2453 foreign_notional: None,
2454 pos_state: None,
2455 pos_cost: None,
2456 pos_cost2: None,
2457 pos_cross: None,
2458 pos_init: None,
2459 pos_comm: None,
2460 pos_loss: None,
2461 pos_margin: None,
2462 pos_maint: None,
2463 pos_allowance: None,
2464 taxable_margin: None,
2465 init_margin: None,
2466 maint_margin: None,
2467 session_margin: None,
2468 target_excess_margin: None,
2469 var_margin: None,
2470 realised_gross_pnl: None,
2471 realised_tax: None,
2472 realised_pnl: None,
2473 unrealised_gross_pnl: None,
2474 long_bankrupt: None,
2475 short_bankrupt: None,
2476 tax_base: None,
2477 indicative_tax_rate: None,
2478 indicative_tax: None,
2479 unrealised_tax: None,
2480 unrealised_pnl: None,
2481 unrealised_pnl_pcnt: None,
2482 unrealised_roe_pcnt: None,
2483 avg_cost_price: None,
2484 avg_entry_price: None,
2485 break_even_price: None,
2486 margin_call_price: None,
2487 liquidation_price: None,
2488 bankrupt_price: None,
2489 last_price: None,
2490 last_value: None,
2491 };
2492
2493 let mut instrument_def = create_test_spot_instrument();
2494 instrument_def.symbol = Ustr::from("SOLUSD");
2495 instrument_def.underlying = Ustr::from("SOL");
2496 instrument_def.quote_currency = Ustr::from("USD");
2497 let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2498
2499 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2500
2501 assert_eq!(report.position_side.as_position_side(), PositionSide::Flat);
2502 assert_eq!(report.quantity.as_f64(), 0.0);
2503 }
2504
2505 #[rstest]
2506 fn test_parse_position_report_spot_scaling() {
2507 let position = BitmexPosition {
2508 account: 789012,
2509 symbol: Ustr::from("SOLUSD"),
2510 current_qty: Some(1000),
2511 timestamp: Some(
2512 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2513 .unwrap()
2514 .with_timezone(&Utc),
2515 ),
2516 currency: None,
2517 underlying: None,
2518 quote_currency: None,
2519 commission: None,
2520 init_margin_req: None,
2521 maint_margin_req: None,
2522 risk_limit: None,
2523 leverage: None,
2524 cross_margin: None,
2525 deleverage_percentile: None,
2526 rebalanced_pnl: None,
2527 prev_realised_pnl: None,
2528 prev_unrealised_pnl: None,
2529 prev_close_price: None,
2530 opening_timestamp: None,
2531 opening_qty: None,
2532 opening_cost: None,
2533 opening_comm: None,
2534 open_order_buy_qty: None,
2535 open_order_buy_cost: None,
2536 open_order_buy_premium: None,
2537 open_order_sell_qty: None,
2538 open_order_sell_cost: None,
2539 open_order_sell_premium: None,
2540 exec_buy_qty: None,
2541 exec_buy_cost: None,
2542 exec_sell_qty: None,
2543 exec_sell_cost: None,
2544 exec_qty: None,
2545 exec_cost: None,
2546 exec_comm: None,
2547 current_timestamp: None,
2548 current_cost: None,
2549 current_comm: None,
2550 realised_cost: None,
2551 unrealised_cost: None,
2552 gross_open_cost: None,
2553 gross_open_premium: None,
2554 gross_exec_cost: None,
2555 is_open: Some(true),
2556 mark_price: None,
2557 mark_value: None,
2558 risk_value: None,
2559 home_notional: None,
2560 foreign_notional: None,
2561 pos_state: None,
2562 pos_cost: None,
2563 pos_cost2: None,
2564 pos_cross: None,
2565 pos_init: None,
2566 pos_comm: None,
2567 pos_loss: None,
2568 pos_margin: None,
2569 pos_maint: None,
2570 pos_allowance: None,
2571 taxable_margin: None,
2572 init_margin: None,
2573 maint_margin: None,
2574 session_margin: None,
2575 target_excess_margin: None,
2576 var_margin: None,
2577 realised_gross_pnl: None,
2578 realised_tax: None,
2579 realised_pnl: None,
2580 unrealised_gross_pnl: None,
2581 long_bankrupt: None,
2582 short_bankrupt: None,
2583 tax_base: None,
2584 indicative_tax_rate: None,
2585 indicative_tax: None,
2586 unrealised_tax: None,
2587 unrealised_pnl: None,
2588 unrealised_pnl_pcnt: None,
2589 unrealised_roe_pcnt: None,
2590 avg_cost_price: None,
2591 avg_entry_price: None,
2592 break_even_price: None,
2593 margin_call_price: None,
2594 liquidation_price: None,
2595 bankrupt_price: None,
2596 last_price: None,
2597 last_value: None,
2598 };
2599
2600 let mut instrument_def = create_test_spot_instrument();
2601 instrument_def.symbol = Ustr::from("SOLUSD");
2602 instrument_def.underlying = Ustr::from("SOL");
2603 instrument_def.quote_currency = Ustr::from("USD");
2604 let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2605
2606 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2607
2608 assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
2609 assert!((report.quantity.as_f64() - 0.1).abs() < 1e-9);
2610 }
2611
2612 fn create_test_spot_instrument() -> BitmexInstrument {
2613 BitmexInstrument {
2614 symbol: Ustr::from("XBTUSD"),
2615 root_symbol: Ustr::from("XBT"),
2616 state: BitmexInstrumentState::Open,
2617 instrument_type: BitmexInstrumentType::Spot,
2618 listing: Some(
2619 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2620 .unwrap()
2621 .with_timezone(&Utc),
2622 ),
2623 front: Some(
2624 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2625 .unwrap()
2626 .with_timezone(&Utc),
2627 ),
2628 expiry: None,
2629 settle: None,
2630 listed_settle: None,
2631 position_currency: Some(Ustr::from("USD")),
2632 underlying: Ustr::from("XBT"),
2633 quote_currency: Ustr::from("USD"),
2634 underlying_symbol: Some(Ustr::from("XBT=")),
2635 reference: Some(Ustr::from("BMEX")),
2636 reference_symbol: Some(Ustr::from(".BXBT")),
2637 lot_size: Some(1000.0),
2638 tick_size: 0.01,
2639 multiplier: 1.0,
2640 settl_currency: Some(Ustr::from("USD")),
2641 is_quanto: false,
2642 is_inverse: false,
2643 maker_fee: Some(-0.00025),
2644 taker_fee: Some(0.00075),
2645 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2646 .unwrap()
2647 .with_timezone(&Utc),
2648 max_order_qty: Some(10000000.0),
2650 max_price: Some(1000000.0),
2651 min_price: None,
2652 settlement_fee: Some(0.0),
2653 mark_price: Some(50500.0),
2654 last_price: Some(50500.0),
2655 bid_price: Some(50499.5),
2656 ask_price: Some(50500.5),
2657 open_interest: Some(0.0),
2658 open_value: Some(0.0),
2659 total_volume: Some(1000000.0),
2660 volume: Some(50000.0),
2661 volume_24h: Some(75000.0),
2662 total_turnover: Some(150000000.0),
2663 turnover: Some(5000000.0),
2664 turnover_24h: Some(7500000.0),
2665 has_liquidity: Some(true),
2666 calc_interval: None,
2668 publish_interval: None,
2669 publish_time: None,
2670 underlying_to_position_multiplier: Some(10000.0),
2671 underlying_to_settle_multiplier: None,
2672 quote_to_settle_multiplier: Some(1.0),
2673 init_margin: Some(0.1),
2674 maint_margin: Some(0.05),
2675 risk_limit: Some(20000000000.0),
2676 risk_step: Some(10000000000.0),
2677 limit: None,
2678 taxed: Some(true),
2679 deleverage: Some(true),
2680 funding_base_symbol: None,
2681 funding_quote_symbol: None,
2682 funding_premium_symbol: None,
2683 funding_timestamp: None,
2684 funding_interval: None,
2685 funding_rate: None,
2686 indicative_funding_rate: None,
2687 rebalance_timestamp: None,
2688 rebalance_interval: None,
2689 prev_close_price: Some(50000.0),
2690 limit_down_price: None,
2691 limit_up_price: None,
2692 prev_total_turnover: Some(100000000.0),
2693 home_notional_24h: Some(1.5),
2694 foreign_notional_24h: Some(75000.0),
2695 prev_price_24h: Some(49500.0),
2696 vwap: Some(50100.0),
2697 high_price: Some(51000.0),
2698 low_price: Some(49000.0),
2699 last_price_protected: Some(50500.0),
2700 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2701 last_change_pcnt: Some(0.0202),
2702 mid_price: Some(50500.0),
2703 impact_bid_price: Some(50490.0),
2704 impact_mid_price: Some(50495.0),
2705 impact_ask_price: Some(50500.0),
2706 fair_method: None,
2707 fair_basis_rate: None,
2708 fair_basis: None,
2709 fair_price: None,
2710 mark_method: Some(BitmexMarkMethod::LastPrice),
2711 indicative_settle_price: None,
2712 settled_price_adjustment_rate: None,
2713 settled_price: None,
2714 instant_pnl: false,
2715 min_tick: None,
2716 funding_base_rate: None,
2717 funding_quote_rate: None,
2718 capped: None,
2719 opening_timestamp: None,
2720 closing_timestamp: None,
2721 prev_total_volume: None,
2722 }
2723 }
2724
2725 fn create_test_perpetual_instrument() -> BitmexInstrument {
2726 BitmexInstrument {
2727 symbol: Ustr::from("XBTUSD"),
2728 root_symbol: Ustr::from("XBT"),
2729 state: BitmexInstrumentState::Open,
2730 instrument_type: BitmexInstrumentType::PerpetualContract,
2731 listing: Some(
2732 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2733 .unwrap()
2734 .with_timezone(&Utc),
2735 ),
2736 front: Some(
2737 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2738 .unwrap()
2739 .with_timezone(&Utc),
2740 ),
2741 expiry: None,
2742 settle: None,
2743 listed_settle: None,
2744 position_currency: Some(Ustr::from("USD")),
2745 underlying: Ustr::from("XBT"),
2746 quote_currency: Ustr::from("USD"),
2747 underlying_symbol: Some(Ustr::from("XBT=")),
2748 reference: Some(Ustr::from("BMEX")),
2749 reference_symbol: Some(Ustr::from(".BXBT")),
2750 lot_size: Some(100.0),
2751 tick_size: 0.5,
2752 multiplier: -100000000.0,
2753 settl_currency: Some(Ustr::from("XBt")),
2754 is_quanto: false,
2755 is_inverse: true,
2756 maker_fee: Some(-0.00025),
2757 taker_fee: Some(0.00075),
2758 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2759 .unwrap()
2760 .with_timezone(&Utc),
2761 max_order_qty: Some(10000000.0),
2763 max_price: Some(1000000.0),
2764 min_price: None,
2765 settlement_fee: Some(0.0),
2766 mark_price: Some(50500.01),
2767 last_price: Some(50500.0),
2768 bid_price: Some(50499.5),
2769 ask_price: Some(50500.5),
2770 open_interest: Some(500000000.0),
2771 open_value: Some(990099009900.0),
2772 total_volume: Some(12345678900000.0),
2773 volume: Some(5000000.0),
2774 volume_24h: Some(75000000.0),
2775 total_turnover: Some(150000000000000.0),
2776 turnover: Some(5000000000.0),
2777 turnover_24h: Some(7500000000.0),
2778 has_liquidity: Some(true),
2779 funding_base_symbol: Some(Ustr::from(".XBTBON8H")),
2781 funding_quote_symbol: Some(Ustr::from(".USDBON8H")),
2782 funding_premium_symbol: Some(Ustr::from(".XBTUSDPI8H")),
2783 funding_timestamp: Some(
2784 DateTime::parse_from_rfc3339("2024-01-01T08:00:00.000Z")
2785 .unwrap()
2786 .with_timezone(&Utc),
2787 ),
2788 funding_interval: Some(
2789 DateTime::parse_from_rfc3339("2000-01-01T08:00:00.000Z")
2790 .unwrap()
2791 .with_timezone(&Utc),
2792 ),
2793 funding_rate: Some(Decimal::from_str("0.0001").unwrap()),
2794 indicative_funding_rate: Some(Decimal::from_str("0.0001").unwrap()),
2795 funding_base_rate: Some(0.01),
2796 funding_quote_rate: Some(-0.01),
2797 calc_interval: None,
2799 publish_interval: None,
2800 publish_time: None,
2801 underlying_to_position_multiplier: None,
2802 underlying_to_settle_multiplier: Some(-100000000.0),
2803 quote_to_settle_multiplier: None,
2804 init_margin: Some(0.01),
2805 maint_margin: Some(0.005),
2806 risk_limit: Some(20000000000.0),
2807 risk_step: Some(10000000000.0),
2808 limit: None,
2809 taxed: Some(true),
2810 deleverage: Some(true),
2811 rebalance_timestamp: None,
2812 rebalance_interval: None,
2813 prev_close_price: Some(50000.0),
2814 limit_down_price: None,
2815 limit_up_price: None,
2816 prev_total_turnover: Some(100000000000000.0),
2817 home_notional_24h: Some(1500.0),
2818 foreign_notional_24h: Some(75000000.0),
2819 prev_price_24h: Some(49500.0),
2820 vwap: Some(50100.0),
2821 high_price: Some(51000.0),
2822 low_price: Some(49000.0),
2823 last_price_protected: Some(50500.0),
2824 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2825 last_change_pcnt: Some(0.0202),
2826 mid_price: Some(50500.0),
2827 impact_bid_price: Some(50490.0),
2828 impact_mid_price: Some(50495.0),
2829 impact_ask_price: Some(50500.0),
2830 fair_method: Some(BitmexFairMethod::FundingRate),
2831 fair_basis_rate: Some(0.1095),
2832 fair_basis: Some(0.01),
2833 fair_price: Some(50500.01),
2834 mark_method: Some(BitmexMarkMethod::FairPrice),
2835 indicative_settle_price: Some(50500.0),
2836 settled_price_adjustment_rate: None,
2837 settled_price: None,
2838 instant_pnl: false,
2839 min_tick: None,
2840 capped: None,
2841 opening_timestamp: None,
2842 closing_timestamp: None,
2843 prev_total_volume: None,
2844 }
2845 }
2846
2847 fn create_test_futures_instrument() -> BitmexInstrument {
2848 BitmexInstrument {
2849 symbol: Ustr::from("XBTH25"),
2850 root_symbol: Ustr::from("XBT"),
2851 state: BitmexInstrumentState::Open,
2852 instrument_type: BitmexInstrumentType::Futures,
2853 listing: Some(
2854 DateTime::parse_from_rfc3339("2024-09-27T12:00:00.000Z")
2855 .unwrap()
2856 .with_timezone(&Utc),
2857 ),
2858 front: Some(
2859 DateTime::parse_from_rfc3339("2024-12-27T12:00:00.000Z")
2860 .unwrap()
2861 .with_timezone(&Utc),
2862 ),
2863 expiry: Some(
2864 DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
2865 .unwrap()
2866 .with_timezone(&Utc),
2867 ),
2868 settle: Some(
2869 DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
2870 .unwrap()
2871 .with_timezone(&Utc),
2872 ),
2873 listed_settle: None,
2874 position_currency: Some(Ustr::from("USD")),
2875 underlying: Ustr::from("XBT"),
2876 quote_currency: Ustr::from("USD"),
2877 underlying_symbol: Some(Ustr::from("XBT=")),
2878 reference: Some(Ustr::from("BMEX")),
2879 reference_symbol: Some(Ustr::from(".BXBT30M")),
2880 lot_size: Some(100.0),
2881 tick_size: 0.5,
2882 multiplier: -100000000.0,
2883 settl_currency: Some(Ustr::from("XBt")),
2884 is_quanto: false,
2885 is_inverse: true,
2886 maker_fee: Some(-0.00025),
2887 taker_fee: Some(0.00075),
2888 settlement_fee: Some(0.0005),
2889 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2890 .unwrap()
2891 .with_timezone(&Utc),
2892 max_order_qty: Some(10000000.0),
2894 max_price: Some(1000000.0),
2895 min_price: None,
2896 mark_price: Some(55500.0),
2897 last_price: Some(55500.0),
2898 bid_price: Some(55499.5),
2899 ask_price: Some(55500.5),
2900 open_interest: Some(50000000.0),
2901 open_value: Some(90090090090.0),
2902 total_volume: Some(1000000000.0),
2903 volume: Some(500000.0),
2904 volume_24h: Some(7500000.0),
2905 total_turnover: Some(15000000000000.0),
2906 turnover: Some(500000000.0),
2907 turnover_24h: Some(750000000.0),
2908 has_liquidity: Some(true),
2909 funding_base_symbol: None,
2911 funding_quote_symbol: None,
2912 funding_premium_symbol: None,
2913 funding_timestamp: None,
2914 funding_interval: None,
2915 funding_rate: None,
2916 indicative_funding_rate: None,
2917 funding_base_rate: None,
2918 funding_quote_rate: None,
2919 calc_interval: None,
2921 publish_interval: None,
2922 publish_time: None,
2923 underlying_to_position_multiplier: None,
2924 underlying_to_settle_multiplier: Some(-100000000.0),
2925 quote_to_settle_multiplier: None,
2926 init_margin: Some(0.02),
2927 maint_margin: Some(0.01),
2928 risk_limit: Some(20000000000.0),
2929 risk_step: Some(10000000000.0),
2930 limit: None,
2931 taxed: Some(true),
2932 deleverage: Some(true),
2933 rebalance_timestamp: None,
2934 rebalance_interval: None,
2935 prev_close_price: Some(55000.0),
2936 limit_down_price: None,
2937 limit_up_price: None,
2938 prev_total_turnover: Some(10000000000000.0),
2939 home_notional_24h: Some(150.0),
2940 foreign_notional_24h: Some(7500000.0),
2941 prev_price_24h: Some(54500.0),
2942 vwap: Some(55100.0),
2943 high_price: Some(56000.0),
2944 low_price: Some(54000.0),
2945 last_price_protected: Some(55500.0),
2946 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2947 last_change_pcnt: Some(0.0183),
2948 mid_price: Some(55500.0),
2949 impact_bid_price: Some(55490.0),
2950 impact_mid_price: Some(55495.0),
2951 impact_ask_price: Some(55500.0),
2952 fair_method: Some(BitmexFairMethod::ImpactMidPrice),
2953 fair_basis_rate: Some(1.8264),
2954 fair_basis: Some(1000.0),
2955 fair_price: Some(55500.0),
2956 mark_method: Some(BitmexMarkMethod::FairPrice),
2957 indicative_settle_price: Some(55500.0),
2958 settled_price_adjustment_rate: None,
2959 settled_price: None,
2960 instant_pnl: false,
2961 min_tick: None,
2962 capped: None,
2963 opening_timestamp: None,
2964 closing_timestamp: None,
2965 prev_total_volume: None,
2966 }
2967 }
2968
2969 #[rstest]
2970 fn test_parse_spot_instrument() {
2971 let instrument = create_test_spot_instrument();
2972 let ts_init = UnixNanos::default();
2973 let result = parse_spot_instrument(&instrument, ts_init).unwrap();
2974
2975 match result {
2977 InstrumentAny::CurrencyPair(spot) => {
2978 assert_eq!(spot.id.symbol.as_str(), "XBTUSD");
2979 assert_eq!(spot.id.venue.as_str(), "BITMEX");
2980 assert_eq!(spot.raw_symbol.as_str(), "XBTUSD");
2981 assert_eq!(spot.price_precision, 2);
2982 assert_eq!(spot.size_precision, 4);
2983 assert_eq!(spot.price_increment.as_f64(), 0.01);
2984 assert!((spot.size_increment.as_f64() - 0.0001).abs() < 1e-9);
2985 assert!((spot.lot_size.unwrap().as_f64() - 0.1).abs() < 1e-9);
2986 assert_eq!(spot.maker_fee.to_f64().unwrap(), -0.00025);
2987 assert_eq!(spot.taker_fee.to_f64().unwrap(), 0.00075);
2988 }
2989 _ => panic!("Expected CurrencyPair variant"),
2990 }
2991 }
2992
2993 #[rstest]
2994 fn test_parse_perpetual_instrument() {
2995 let instrument = create_test_perpetual_instrument();
2996 let ts_init = UnixNanos::default();
2997 let result = parse_perpetual_instrument(&instrument, ts_init).unwrap();
2998
2999 match result {
3001 InstrumentAny::CryptoPerpetual(perp) => {
3002 assert_eq!(perp.id.symbol.as_str(), "XBTUSD");
3003 assert_eq!(perp.id.venue.as_str(), "BITMEX");
3004 assert_eq!(perp.raw_symbol.as_str(), "XBTUSD");
3005 assert_eq!(perp.price_precision, 1);
3006 assert_eq!(perp.size_precision, 0);
3007 assert_eq!(perp.price_increment.as_f64(), 0.5);
3008 assert_eq!(perp.size_increment.as_f64(), 1.0);
3009 assert_eq!(perp.maker_fee.to_f64().unwrap(), -0.00025);
3010 assert_eq!(perp.taker_fee.to_f64().unwrap(), 0.00075);
3011 assert!(perp.is_inverse);
3012 }
3013 _ => panic!("Expected CryptoPerpetual variant"),
3014 }
3015 }
3016
3017 #[rstest]
3018 fn test_parse_futures_instrument() {
3019 let instrument = create_test_futures_instrument();
3020 let ts_init = UnixNanos::default();
3021 let result = parse_futures_instrument(&instrument, ts_init).unwrap();
3022
3023 match result {
3025 InstrumentAny::CryptoFuture(instrument) => {
3026 assert_eq!(instrument.id.symbol.as_str(), "XBTH25");
3027 assert_eq!(instrument.id.venue.as_str(), "BITMEX");
3028 assert_eq!(instrument.raw_symbol.as_str(), "XBTH25");
3029 assert_eq!(instrument.underlying.code.as_str(), "XBT");
3030 assert_eq!(instrument.price_precision, 1);
3031 assert_eq!(instrument.size_precision, 0);
3032 assert_eq!(instrument.price_increment.as_f64(), 0.5);
3033 assert_eq!(instrument.size_increment.as_f64(), 1.0);
3034 assert_eq!(instrument.maker_fee.to_f64().unwrap(), -0.00025);
3035 assert_eq!(instrument.taker_fee.to_f64().unwrap(), 0.00075);
3036 assert!(instrument.is_inverse);
3037 assert!(instrument.expiration_ns.as_u64() > 0);
3040 }
3041 _ => panic!("Expected CryptoFuture variant"),
3042 }
3043 }
3044
3045 #[rstest]
3046 fn test_parse_order_status_report_missing_ord_status_infers_filled() {
3047 let order = BitmexOrder {
3048 account: 123456,
3049 symbol: Some(Ustr::from("XBTUSD")),
3050 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3051 cl_ord_id: Some(Ustr::from("client-filled")),
3052 cl_ord_link_id: None,
3053 side: Some(BitmexSide::Buy),
3054 ord_type: Some(BitmexOrderType::Limit),
3055 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3056 ord_status: None, order_qty: Some(100),
3058 cum_qty: Some(100), price: Some(50000.0),
3060 stop_px: None,
3061 display_qty: None,
3062 peg_offset_value: None,
3063 peg_price_type: None,
3064 currency: Some(Ustr::from("USD")),
3065 settl_currency: Some(Ustr::from("XBt")),
3066 exec_inst: None,
3067 contingency_type: None,
3068 ex_destination: None,
3069 triggered: None,
3070 working_indicator: Some(false),
3071 ord_rej_reason: None,
3072 leaves_qty: Some(0), avg_px: Some(50050.0),
3074 multi_leg_reporting_type: None,
3075 text: None,
3076 transact_time: Some(
3077 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3078 .unwrap()
3079 .with_timezone(&Utc),
3080 ),
3081 timestamp: Some(
3082 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3083 .unwrap()
3084 .with_timezone(&Utc),
3085 ),
3086 };
3087
3088 let instrument =
3089 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3090 .unwrap();
3091 let report =
3092 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3093 .unwrap();
3094
3095 assert_eq!(report.order_status, OrderStatus::Filled);
3096 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
3097 assert_eq!(report.filled_qty.as_f64(), 100.0);
3098 }
3099
3100 #[rstest]
3101 fn test_parse_order_status_report_missing_ord_status_infers_canceled() {
3102 let order = BitmexOrder {
3103 account: 123456,
3104 symbol: Some(Ustr::from("XBTUSD")),
3105 order_id: Uuid::parse_str("b2c3d4e5-f6a7-8901-bcde-f12345678901").unwrap(),
3106 cl_ord_id: Some(Ustr::from("client-canceled")),
3107 cl_ord_link_id: None,
3108 side: Some(BitmexSide::Sell),
3109 ord_type: Some(BitmexOrderType::Limit),
3110 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3111 ord_status: None, order_qty: Some(200),
3113 cum_qty: Some(0), price: Some(60000.0),
3115 stop_px: None,
3116 display_qty: None,
3117 peg_offset_value: None,
3118 peg_price_type: None,
3119 currency: Some(Ustr::from("USD")),
3120 settl_currency: Some(Ustr::from("XBt")),
3121 exec_inst: None,
3122 contingency_type: None,
3123 ex_destination: None,
3124 triggered: None,
3125 working_indicator: Some(false),
3126 ord_rej_reason: None,
3127 leaves_qty: Some(0), avg_px: None,
3129 multi_leg_reporting_type: None,
3130 text: Some(Ustr::from("Canceled: Already filled")),
3131 transact_time: Some(
3132 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3133 .unwrap()
3134 .with_timezone(&Utc),
3135 ),
3136 timestamp: Some(
3137 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3138 .unwrap()
3139 .with_timezone(&Utc),
3140 ),
3141 };
3142
3143 let instrument =
3144 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3145 .unwrap();
3146 let report =
3147 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3148 .unwrap();
3149
3150 assert_eq!(report.order_status, OrderStatus::Canceled);
3151 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
3152 assert_eq!(report.filled_qty.as_f64(), 0.0);
3153 assert_eq!(
3155 report.cancel_reason.as_ref().unwrap(),
3156 "Canceled: Already filled"
3157 );
3158 }
3159
3160 #[rstest]
3161 fn test_parse_order_status_report_missing_ord_status_with_leaves_qty_fails() {
3162 let order = BitmexOrder {
3163 account: 123456,
3164 symbol: Some(Ustr::from("XBTUSD")),
3165 order_id: Uuid::parse_str("c3d4e5f6-a7b8-9012-cdef-123456789012").unwrap(),
3166 cl_ord_id: Some(Ustr::from("client-partial")),
3167 cl_ord_link_id: None,
3168 side: Some(BitmexSide::Buy),
3169 ord_type: Some(BitmexOrderType::Limit),
3170 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3171 ord_status: None, order_qty: Some(100),
3173 cum_qty: Some(50),
3174 price: Some(50000.0),
3175 stop_px: None,
3176 display_qty: None,
3177 peg_offset_value: None,
3178 peg_price_type: None,
3179 currency: Some(Ustr::from("USD")),
3180 settl_currency: Some(Ustr::from("XBt")),
3181 exec_inst: None,
3182 contingency_type: None,
3183 ex_destination: None,
3184 triggered: None,
3185 working_indicator: Some(true),
3186 ord_rej_reason: None,
3187 leaves_qty: Some(50), avg_px: None,
3189 multi_leg_reporting_type: None,
3190 text: None,
3191 transact_time: Some(
3192 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3193 .unwrap()
3194 .with_timezone(&Utc),
3195 ),
3196 timestamp: Some(
3197 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3198 .unwrap()
3199 .with_timezone(&Utc),
3200 ),
3201 };
3202
3203 let instrument =
3204 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3205 .unwrap();
3206 let result =
3207 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
3208
3209 assert!(result.is_err());
3210 let err_msg = result.unwrap_err().to_string();
3211 assert!(err_msg.contains("missing ord_status"));
3212 assert!(err_msg.contains("cannot infer"));
3213 }
3214
3215 #[rstest]
3216 fn test_parse_order_status_report_missing_ord_status_no_quantities_fails() {
3217 let order = BitmexOrder {
3218 account: 123456,
3219 symbol: Some(Ustr::from("XBTUSD")),
3220 order_id: Uuid::parse_str("d4e5f6a7-b8c9-0123-def0-123456789013").unwrap(),
3221 cl_ord_id: Some(Ustr::from("client-unknown")),
3222 cl_ord_link_id: None,
3223 side: Some(BitmexSide::Buy),
3224 ord_type: Some(BitmexOrderType::Limit),
3225 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3226 ord_status: None, order_qty: Some(100),
3228 cum_qty: None, price: Some(50000.0),
3230 stop_px: None,
3231 display_qty: None,
3232 peg_offset_value: None,
3233 peg_price_type: None,
3234 currency: Some(Ustr::from("USD")),
3235 settl_currency: Some(Ustr::from("XBt")),
3236 exec_inst: None,
3237 contingency_type: None,
3238 ex_destination: None,
3239 triggered: None,
3240 working_indicator: Some(true),
3241 ord_rej_reason: None,
3242 leaves_qty: None, avg_px: None,
3244 multi_leg_reporting_type: None,
3245 text: None,
3246 transact_time: Some(
3247 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3248 .unwrap()
3249 .with_timezone(&Utc),
3250 ),
3251 timestamp: Some(
3252 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3253 .unwrap()
3254 .with_timezone(&Utc),
3255 ),
3256 };
3257
3258 let instrument =
3259 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3260 .unwrap();
3261 let result =
3262 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
3263
3264 assert!(result.is_err());
3265 let err_msg = result.unwrap_err().to_string();
3266 assert!(err_msg.contains("missing ord_status"));
3267 assert!(err_msg.contains("cannot infer"));
3268 }
3269
3270 #[rstest]
3271 fn test_parse_order_status_report_infers_market_order_type() {
3272 let order = BitmexOrder {
3274 account: 123456,
3275 symbol: Some(Ustr::from("XBTUSD")),
3276 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3277 cl_ord_id: Some(Ustr::from("client-123")),
3278 cl_ord_link_id: None,
3279 side: Some(BitmexSide::Buy),
3280 ord_type: None,
3281 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3282 ord_status: Some(BitmexOrderStatus::Filled),
3283 order_qty: Some(100),
3284 cum_qty: Some(100),
3285 price: None,
3286 stop_px: None,
3287 display_qty: None,
3288 peg_offset_value: None,
3289 peg_price_type: None,
3290 currency: Some(Ustr::from("USD")),
3291 settl_currency: Some(Ustr::from("XBt")),
3292 exec_inst: None,
3293 contingency_type: None,
3294 ex_destination: None,
3295 triggered: None,
3296 working_indicator: None,
3297 ord_rej_reason: None,
3298 leaves_qty: Some(0),
3299 avg_px: Some(50000.0),
3300 multi_leg_reporting_type: None,
3301 text: None,
3302 transact_time: Some(
3303 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3304 .unwrap()
3305 .with_timezone(&Utc),
3306 ),
3307 timestamp: Some(
3308 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3309 .unwrap()
3310 .with_timezone(&Utc),
3311 ),
3312 };
3313
3314 let instrument =
3315 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3316 .unwrap();
3317 let report =
3318 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3319 .unwrap();
3320
3321 assert_eq!(report.order_type, OrderType::Market);
3322 }
3323
3324 #[rstest]
3325 fn test_parse_order_status_report_infers_limit_order_type() {
3326 let order = BitmexOrder {
3328 account: 123456,
3329 symbol: Some(Ustr::from("XBTUSD")),
3330 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3331 cl_ord_id: Some(Ustr::from("client-123")),
3332 cl_ord_link_id: None,
3333 side: Some(BitmexSide::Buy),
3334 ord_type: None,
3335 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3336 ord_status: Some(BitmexOrderStatus::New),
3337 order_qty: Some(100),
3338 cum_qty: Some(0),
3339 price: Some(50000.0),
3340 stop_px: None,
3341 display_qty: None,
3342 peg_offset_value: None,
3343 peg_price_type: None,
3344 currency: Some(Ustr::from("USD")),
3345 settl_currency: Some(Ustr::from("XBt")),
3346 exec_inst: None,
3347 contingency_type: None,
3348 ex_destination: None,
3349 triggered: None,
3350 working_indicator: Some(true),
3351 ord_rej_reason: None,
3352 leaves_qty: Some(100),
3353 avg_px: None,
3354 multi_leg_reporting_type: None,
3355 text: None,
3356 transact_time: Some(
3357 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3358 .unwrap()
3359 .with_timezone(&Utc),
3360 ),
3361 timestamp: Some(
3362 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3363 .unwrap()
3364 .with_timezone(&Utc),
3365 ),
3366 };
3367
3368 let instrument =
3369 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3370 .unwrap();
3371 let report =
3372 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3373 .unwrap();
3374
3375 assert_eq!(report.order_type, OrderType::Limit);
3376 }
3377
3378 #[rstest]
3379 fn test_parse_order_status_report_infers_stop_market_order_type() {
3380 let order = BitmexOrder {
3382 account: 123456,
3383 symbol: Some(Ustr::from("XBTUSD")),
3384 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3385 cl_ord_id: Some(Ustr::from("client-123")),
3386 cl_ord_link_id: None,
3387 side: Some(BitmexSide::Sell),
3388 ord_type: None,
3389 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3390 ord_status: Some(BitmexOrderStatus::New),
3391 order_qty: Some(100),
3392 cum_qty: Some(0),
3393 price: None,
3394 stop_px: Some(45000.0),
3395 display_qty: None,
3396 peg_offset_value: None,
3397 peg_price_type: None,
3398 currency: Some(Ustr::from("USD")),
3399 settl_currency: Some(Ustr::from("XBt")),
3400 exec_inst: None,
3401 contingency_type: None,
3402 ex_destination: None,
3403 triggered: None,
3404 working_indicator: Some(false),
3405 ord_rej_reason: None,
3406 leaves_qty: Some(100),
3407 avg_px: None,
3408 multi_leg_reporting_type: None,
3409 text: None,
3410 transact_time: Some(
3411 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3412 .unwrap()
3413 .with_timezone(&Utc),
3414 ),
3415 timestamp: Some(
3416 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3417 .unwrap()
3418 .with_timezone(&Utc),
3419 ),
3420 };
3421
3422 let instrument =
3423 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3424 .unwrap();
3425 let report =
3426 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3427 .unwrap();
3428
3429 assert_eq!(report.order_type, OrderType::StopMarket);
3430 }
3431
3432 #[rstest]
3433 fn test_parse_order_status_report_infers_stop_limit_order_type() {
3434 let order = BitmexOrder {
3436 account: 123456,
3437 symbol: Some(Ustr::from("XBTUSD")),
3438 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3439 cl_ord_id: Some(Ustr::from("client-123")),
3440 cl_ord_link_id: None,
3441 side: Some(BitmexSide::Sell),
3442 ord_type: None,
3443 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3444 ord_status: Some(BitmexOrderStatus::New),
3445 order_qty: Some(100),
3446 cum_qty: Some(0),
3447 price: Some(44000.0),
3448 stop_px: Some(45000.0),
3449 display_qty: None,
3450 peg_offset_value: None,
3451 peg_price_type: None,
3452 currency: Some(Ustr::from("USD")),
3453 settl_currency: Some(Ustr::from("XBt")),
3454 exec_inst: None,
3455 contingency_type: None,
3456 ex_destination: None,
3457 triggered: None,
3458 working_indicator: Some(false),
3459 ord_rej_reason: None,
3460 leaves_qty: Some(100),
3461 avg_px: None,
3462 multi_leg_reporting_type: None,
3463 text: None,
3464 transact_time: Some(
3465 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3466 .unwrap()
3467 .with_timezone(&Utc),
3468 ),
3469 timestamp: Some(
3470 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3471 .unwrap()
3472 .with_timezone(&Utc),
3473 ),
3474 };
3475
3476 let instrument =
3477 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3478 .unwrap();
3479 let report =
3480 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3481 .unwrap();
3482
3483 assert_eq!(report.order_type, OrderType::StopLimit);
3484 }
3485
3486 #[rstest]
3487 fn test_parse_order_status_report_uses_cached_order_type() {
3488 let order = BitmexOrder {
3490 account: 123456,
3491 symbol: Some(Ustr::from("XBTUSD")),
3492 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3493 cl_ord_id: Some(Ustr::from("client-123")),
3494 cl_ord_link_id: None,
3495 side: Some(BitmexSide::Buy),
3496 ord_type: None,
3497 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3498 ord_status: Some(BitmexOrderStatus::Canceled),
3499 order_qty: None,
3500 cum_qty: Some(0),
3501 price: None,
3502 stop_px: None,
3503 display_qty: None,
3504 peg_offset_value: None,
3505 peg_price_type: None,
3506 currency: Some(Ustr::from("USD")),
3507 settl_currency: Some(Ustr::from("XBt")),
3508 exec_inst: None,
3509 contingency_type: None,
3510 ex_destination: None,
3511 triggered: None,
3512 working_indicator: None,
3513 ord_rej_reason: None,
3514 leaves_qty: Some(0),
3515 avg_px: None,
3516 multi_leg_reporting_type: None,
3517 text: None,
3518 transact_time: Some(
3519 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
3520 .unwrap()
3521 .with_timezone(&Utc),
3522 ),
3523 timestamp: Some(
3524 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
3525 .unwrap()
3526 .with_timezone(&Utc),
3527 ),
3528 };
3529
3530 let instrument =
3531 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3532 .unwrap();
3533
3534 let cache: DashMap<ClientOrderId, OrderType> = DashMap::new();
3536 cache.insert(ClientOrderId::new("client-123"), OrderType::StopLimit);
3537
3538 let report =
3539 parse_order_status_report(&order, &instrument, &cache, UnixNanos::from(1)).unwrap();
3540
3541 assert_eq!(report.order_type, OrderType::StopLimit);
3542 }
3543}