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