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