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