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