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