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