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