1use std::str::FromStr;
19
20use dashmap::DashMap;
21use nautilus_core::{UnixNanos, uuid::UUID4};
22use nautilus_model::{
23 data::{Bar, BarType, TradeTick},
24 enums::{ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType},
25 identifiers::{ClientOrderId, OrderListId, Symbol, TradeId, VenueOrderId},
26 instruments::{
27 CryptoFuture, CryptoFuturesSpread, CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
28 },
29 reports::{FillReport, OrderStatusReport, PositionStatusReport},
30 types::{Currency, Money, Price, Quantity, fixed::FIXED_PRECISION},
31};
32use rust_decimal::Decimal;
33use ustr::Ustr;
34
35use super::models::{
36 BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTrade, BitmexTradeBin,
37};
38use crate::common::{
39 enums::{
40 BitmexExecInstruction, BitmexExecType, BitmexInstrumentState, BitmexInstrumentType,
41 BitmexOrderType, BitmexPegPriceType,
42 },
43 parse::{
44 bitmex_account_id, bitmex_currency_divisor, clean_reason, convert_contract_quantity,
45 derive_contract_decimal_and_increment, derive_trade_id, extract_trigger_type,
46 map_bitmex_currency, normalize_trade_bin_prices, normalize_trade_bin_volume,
47 parse_aggressor_side, parse_contracts_quantity, parse_instrument_id, parse_liquidity_side,
48 parse_optional_datetime_to_unix_nanos, parse_position_side,
49 parse_signed_contracts_quantity,
50 },
51};
52
53#[derive(Debug)]
55pub enum InstrumentParseResult {
56 Ok(Box<InstrumentAny>),
58 Unsupported {
60 symbol: String,
61 instrument_type: BitmexInstrumentType,
62 },
63 Inactive {
65 symbol: String,
66 state: BitmexInstrumentState,
67 },
68 Failed {
70 symbol: String,
71 instrument_type: BitmexInstrumentType,
72 error: String,
73 },
74}
75
76fn get_position_multiplier(definition: &BitmexInstrument) -> Option<f64> {
82 if definition.is_inverse {
83 definition
84 .underlying_to_settle_multiplier
85 .or(definition.underlying_to_position_multiplier)
86 } else {
87 definition.underlying_to_position_multiplier
88 }
89}
90
91#[must_use]
93pub fn parse_instrument_any(
94 instrument: &BitmexInstrument,
95 ts_init: UnixNanos,
96) -> InstrumentParseResult {
97 let symbol = instrument.symbol.to_string();
98 let instrument_type = instrument.instrument_type;
99
100 match instrument.state {
101 BitmexInstrumentState::Open | BitmexInstrumentState::Closed => {}
102 state @ (BitmexInstrumentState::Unlisted
103 | BitmexInstrumentState::Settled
104 | BitmexInstrumentState::Delisted
105 | BitmexInstrumentState::Unknown) => {
106 return InstrumentParseResult::Inactive { symbol, state };
107 }
108 }
109
110 match instrument.instrument_type {
111 BitmexInstrumentType::Spot => match parse_spot_instrument(instrument, ts_init) {
112 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
113 Err(e) => InstrumentParseResult::Failed {
114 symbol,
115 instrument_type,
116 error: e.to_string(),
117 },
118 },
119 BitmexInstrumentType::PerpetualContract | BitmexInstrumentType::PerpetualContractFx => {
120 match parse_perpetual_instrument(instrument, ts_init) {
122 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
123 Err(e) => InstrumentParseResult::Failed {
124 symbol,
125 instrument_type,
126 error: e.to_string(),
127 },
128 }
129 }
130 BitmexInstrumentType::Futures => match parse_futures_instrument(instrument, ts_init) {
131 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
132 Err(e) => InstrumentParseResult::Failed {
133 symbol,
134 instrument_type,
135 error: e.to_string(),
136 },
137 },
138 BitmexInstrumentType::FuturesSpread | BitmexInstrumentType::FuturesSpreads => {
139 match parse_crypto_futures_spread_instrument(instrument, ts_init) {
140 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
141 Err(e) => InstrumentParseResult::Failed {
142 symbol,
143 instrument_type,
144 error: e.to_string(),
145 },
146 }
147 }
148 BitmexInstrumentType::PredictionMarket
149 | BitmexInstrumentType::LegacyFutures
150 | BitmexInstrumentType::LegacyFuturesN => {
151 match parse_futures_instrument(instrument, ts_init) {
153 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
154 Err(e) => InstrumentParseResult::Failed {
155 symbol,
156 instrument_type,
157 error: e.to_string(),
158 },
159 }
160 }
161 BitmexInstrumentType::BasketIndex
162 | BitmexInstrumentType::CryptoIndex
163 | BitmexInstrumentType::FxIndex
164 | BitmexInstrumentType::LendingIndex
165 | BitmexInstrumentType::ReferenceBasket
166 | BitmexInstrumentType::VolatilityIndex
167 | BitmexInstrumentType::StockIndex
168 | BitmexInstrumentType::YieldIndex => {
169 match parse_index_instrument(instrument, ts_init) {
172 Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
173 Err(e) => InstrumentParseResult::Failed {
174 symbol,
175 instrument_type,
176 error: e.to_string(),
177 },
178 }
179 }
180
181 BitmexInstrumentType::TradFiPerpetual
186 | BitmexInstrumentType::CallOption
187 | BitmexInstrumentType::PutOption
188 | BitmexInstrumentType::SwapRate
189 | BitmexInstrumentType::Other => InstrumentParseResult::Unsupported {
190 symbol,
191 instrument_type,
192 },
193 }
194}
195
196pub fn parse_index_instrument(
209 definition: &BitmexInstrument,
210 ts_init: UnixNanos,
211) -> anyhow::Result<InstrumentAny> {
212 let instrument_id = parse_instrument_id(definition.symbol);
213 let raw_symbol = Symbol::new(definition.symbol);
214
215 let base_currency = Currency::USD();
216 let quote_currency = Currency::USD();
217 let settlement_currency = Currency::USD();
218
219 let price_increment = Price::from(definition.tick_size.to_string());
220 let size_increment = Quantity::from(1); Ok(InstrumentAny::CryptoPerpetual(
223 CryptoPerpetual::builder()
224 .instrument_id(instrument_id)
225 .raw_symbol(raw_symbol)
226 .base_currency(base_currency)
227 .quote_currency(quote_currency)
228 .settlement_currency(settlement_currency)
229 .is_inverse(false)
230 .price_precision(price_increment.precision)
231 .size_precision(size_increment.precision)
232 .price_increment(price_increment)
233 .size_increment(size_increment)
234 .ts_event(ts_init)
235 .ts_init(ts_init)
236 .build()
237 .unwrap(),
238 ))
239}
240
241pub fn parse_spot_instrument(
251 definition: &BitmexInstrument,
252 ts_init: UnixNanos,
253) -> anyhow::Result<InstrumentAny> {
254 let instrument_id = parse_instrument_id(definition.symbol);
255 let raw_symbol = Symbol::new(definition.symbol);
256 let base_currency = get_currency(&definition.underlying.to_uppercase());
257 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
258
259 let price_increment = Price::from(definition.tick_size.to_string());
260
261 let max_scale = FIXED_PRECISION as u32;
262 let (contract_decimal, size_increment) =
263 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
264
265 let min_quantity = convert_contract_quantity(
266 definition.lot_size,
267 contract_decimal,
268 max_scale,
269 "minimum quantity",
270 )?;
271
272 let taker_fee = definition
273 .taker_fee
274 .and_then(|fee| Decimal::try_from(fee).ok())
275 .unwrap_or(Decimal::ZERO);
276 let maker_fee = definition
277 .maker_fee
278 .and_then(|fee| Decimal::try_from(fee).ok())
279 .unwrap_or(Decimal::ZERO);
280
281 let margin_init = definition
282 .init_margin
283 .as_ref()
284 .and_then(|margin| Decimal::try_from(*margin).ok())
285 .unwrap_or(Decimal::ZERO);
286 let margin_maint = definition
287 .maint_margin
288 .as_ref()
289 .and_then(|margin| Decimal::try_from(*margin).ok())
290 .unwrap_or(Decimal::ZERO);
291
292 let lot_size =
293 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
294 let max_quantity = convert_contract_quantity(
295 definition.max_order_qty,
296 contract_decimal,
297 max_scale,
298 "max quantity",
299 )?;
300 let max_notional: Option<Money> = None;
301 let min_notional: Option<Money> = None;
302 let max_price = definition
303 .max_price
304 .map(|price| Price::from(price.to_string()));
305 let min_price = definition
306 .min_price
307 .map(|price| Price::from(price.to_string()));
308 let ts_event = UnixNanos::from(definition.timestamp);
309
310 let instrument = CurrencyPair::builder()
311 .instrument_id(instrument_id)
312 .raw_symbol(raw_symbol)
313 .base_currency(base_currency)
314 .quote_currency(quote_currency)
315 .price_precision(price_increment.precision)
316 .size_precision(size_increment.precision)
317 .price_increment(price_increment)
318 .size_increment(size_increment)
319 .maybe_lot_size(lot_size)
320 .maybe_max_quantity(max_quantity)
321 .maybe_min_quantity(min_quantity)
322 .maybe_max_notional(max_notional)
323 .maybe_min_notional(min_notional)
324 .maybe_max_price(max_price)
325 .maybe_min_price(min_price)
326 .margin_init(margin_init)
327 .margin_maint(margin_maint)
328 .maker_fee(maker_fee)
329 .taker_fee(taker_fee)
330 .ts_event(ts_event)
331 .ts_init(ts_init)
332 .build()
333 .unwrap();
334
335 Ok(InstrumentAny::CurrencyPair(instrument))
336}
337
338pub fn parse_perpetual_instrument(
348 definition: &BitmexInstrument,
349 ts_init: UnixNanos,
350) -> anyhow::Result<InstrumentAny> {
351 let instrument_id = parse_instrument_id(definition.symbol);
352 let raw_symbol = Symbol::new(definition.symbol);
353 let base_currency = get_currency(&definition.underlying.to_uppercase());
354 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
355 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
356 || definition.quote_currency.to_uppercase(),
357 |s| s.to_uppercase(),
358 ));
359 let is_inverse = definition.is_inverse;
360
361 let price_increment = Price::from(definition.tick_size.to_string());
362
363 let max_scale = FIXED_PRECISION as u32;
364 let (contract_decimal, size_increment) =
365 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
366
367 let lot_size =
368 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
369
370 let taker_fee = definition
371 .taker_fee
372 .and_then(|fee| Decimal::try_from(fee).ok())
373 .unwrap_or(Decimal::ZERO);
374 let maker_fee = definition
375 .maker_fee
376 .and_then(|fee| Decimal::try_from(fee).ok())
377 .unwrap_or(Decimal::ZERO);
378
379 let margin_init = definition
380 .init_margin
381 .as_ref()
382 .and_then(|margin| Decimal::try_from(*margin).ok())
383 .unwrap_or(Decimal::ZERO);
384 let margin_maint = definition
385 .maint_margin
386 .as_ref()
387 .and_then(|margin| Decimal::try_from(*margin).ok())
388 .unwrap_or(Decimal::ZERO);
389
390 let multiplier = Some(parse_instrument_multiplier(
391 definition,
392 settlement_currency,
393 )?);
394 let max_quantity = convert_contract_quantity(
395 definition.max_order_qty,
396 contract_decimal,
397 max_scale,
398 "max quantity",
399 )?;
400 let min_quantity = lot_size;
401 let max_notional: Option<Money> = None;
402 let min_notional: Option<Money> = None;
403 let max_price = definition
404 .max_price
405 .map(|price| Price::from(price.to_string()));
406 let min_price = definition
407 .min_price
408 .map(|price| Price::from(price.to_string()));
409 let ts_event = UnixNanos::from(definition.timestamp);
410
411 let instrument = CryptoPerpetual::builder()
412 .instrument_id(instrument_id)
413 .raw_symbol(raw_symbol)
414 .base_currency(base_currency)
415 .quote_currency(quote_currency)
416 .settlement_currency(settlement_currency)
417 .is_inverse(is_inverse)
418 .price_precision(price_increment.precision)
419 .size_precision(size_increment.precision)
420 .price_increment(price_increment)
421 .size_increment(size_increment)
422 .maybe_multiplier(multiplier)
423 .maybe_lot_size(lot_size)
424 .maybe_max_quantity(max_quantity)
425 .maybe_min_quantity(min_quantity)
426 .maybe_max_notional(max_notional)
427 .maybe_min_notional(min_notional)
428 .maybe_max_price(max_price)
429 .maybe_min_price(min_price)
430 .margin_init(margin_init)
431 .margin_maint(margin_maint)
432 .maker_fee(maker_fee)
433 .taker_fee(taker_fee)
434 .ts_event(ts_event)
435 .ts_init(ts_init)
436 .build()
437 .unwrap();
438
439 Ok(InstrumentAny::CryptoPerpetual(instrument))
440}
441
442pub fn parse_futures_instrument(
452 definition: &BitmexInstrument,
453 ts_init: UnixNanos,
454) -> anyhow::Result<InstrumentAny> {
455 let instrument_id = parse_instrument_id(definition.symbol);
456 let raw_symbol = Symbol::new(definition.symbol);
457 let underlying = get_currency(&definition.underlying.to_uppercase());
458 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
459 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
460 || definition.quote_currency.to_uppercase(),
461 |s| s.to_uppercase(),
462 ));
463 let is_inverse = definition.is_inverse;
464
465 let ts_event = UnixNanos::from(definition.timestamp);
466 let activation_ns = definition
467 .listing
468 .as_ref()
469 .map_or(ts_event, |dt| UnixNanos::from(*dt));
470 let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
471 let price_increment = Price::from(definition.tick_size.to_string());
472
473 let max_scale = FIXED_PRECISION as u32;
474 let (contract_decimal, size_increment) =
475 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
476
477 let lot_size =
478 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
479
480 let taker_fee = definition
481 .taker_fee
482 .and_then(|fee| Decimal::try_from(fee).ok())
483 .unwrap_or(Decimal::ZERO);
484 let maker_fee = definition
485 .maker_fee
486 .and_then(|fee| Decimal::try_from(fee).ok())
487 .unwrap_or(Decimal::ZERO);
488
489 let margin_init = definition
490 .init_margin
491 .as_ref()
492 .and_then(|margin| Decimal::try_from(*margin).ok())
493 .unwrap_or(Decimal::ZERO);
494 let margin_maint = definition
495 .maint_margin
496 .as_ref()
497 .and_then(|margin| Decimal::try_from(*margin).ok())
498 .unwrap_or(Decimal::ZERO);
499
500 let multiplier = Some(parse_instrument_multiplier(
501 definition,
502 settlement_currency,
503 )?);
504
505 let max_quantity = convert_contract_quantity(
506 definition.max_order_qty,
507 contract_decimal,
508 max_scale,
509 "max quantity",
510 )?;
511 let min_quantity = lot_size;
512 let max_notional: Option<Money> = None;
513 let min_notional: Option<Money> = None;
514 let max_price = definition
515 .max_price
516 .map(|price| Price::from(price.to_string()));
517 let min_price = definition
518 .min_price
519 .map(|price| Price::from(price.to_string()));
520
521 let instrument = CryptoFuture::builder()
522 .instrument_id(instrument_id)
523 .raw_symbol(raw_symbol)
524 .underlying(underlying)
525 .quote_currency(quote_currency)
526 .settlement_currency(settlement_currency)
527 .is_inverse(is_inverse)
528 .activation_ns(activation_ns)
529 .expiration_ns(expiration_ns)
530 .price_precision(price_increment.precision)
531 .size_precision(size_increment.precision)
532 .price_increment(price_increment)
533 .size_increment(size_increment)
534 .maybe_multiplier(multiplier)
535 .maybe_lot_size(lot_size)
536 .maybe_max_quantity(max_quantity)
537 .maybe_min_quantity(min_quantity)
538 .maybe_max_notional(max_notional)
539 .maybe_min_notional(min_notional)
540 .maybe_max_price(max_price)
541 .maybe_min_price(min_price)
542 .margin_init(margin_init)
543 .margin_maint(margin_maint)
544 .maker_fee(maker_fee)
545 .taker_fee(taker_fee)
546 .ts_event(ts_event)
547 .ts_init(ts_init)
548 .build()
549 .unwrap();
550
551 Ok(InstrumentAny::CryptoFuture(instrument))
552}
553
554fn parse_instrument_multiplier(
555 definition: &BitmexInstrument,
556 settlement_currency: Currency,
557) -> anyhow::Result<Quantity> {
558 if !definition.is_quanto {
559 return Quantity::new_checked(definition.multiplier.abs(), 0).map_err(Into::into);
560 }
561
562 let raw = Decimal::try_from(definition.multiplier.abs())
563 .map_err(|e| anyhow::anyhow!("Invalid multiplier {}: {e}", definition.multiplier))?;
564 let bitmex_currency = definition
565 .settl_currency
566 .as_ref()
567 .unwrap_or(&definition.quote_currency);
568 let divisor = bitmex_currency_divisor(bitmex_currency.as_str());
569 let value = raw.checked_div(divisor).ok_or_else(|| {
570 anyhow::anyhow!("Invalid multiplier divisor {divisor} for {bitmex_currency}")
571 })?;
572
573 Quantity::from_decimal_dp(value, settlement_currency.precision).map_err(Into::into)
574}
575
576pub fn parse_crypto_futures_spread_instrument(
586 definition: &BitmexInstrument,
587 ts_init: UnixNanos,
588) -> anyhow::Result<InstrumentAny> {
589 let instrument_id = parse_instrument_id(definition.symbol);
590 let raw_symbol = Symbol::new(definition.symbol);
591 let underlying = get_currency(&definition.underlying.to_uppercase());
592 let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
593 let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
594 || definition.quote_currency.to_uppercase(),
595 |s| s.to_uppercase(),
596 ));
597 let is_inverse = definition.is_inverse;
598
599 let ts_event = UnixNanos::from(definition.timestamp);
600 let activation_ns = definition
601 .listing
602 .as_ref()
603 .map_or(ts_event, |dt| UnixNanos::from(*dt));
604 let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
605 let price_increment = Price::from(definition.tick_size.to_string());
606
607 let max_scale = FIXED_PRECISION as u32;
608 let (contract_decimal, size_increment) =
609 derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
610
611 let lot_size =
612 convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
613
614 let taker_fee = definition
615 .taker_fee
616 .and_then(|fee| Decimal::try_from(fee).ok())
617 .unwrap_or(Decimal::ZERO);
618 let maker_fee = definition
619 .maker_fee
620 .and_then(|fee| Decimal::try_from(fee).ok())
621 .unwrap_or(Decimal::ZERO);
622
623 let margin_init = definition
624 .init_margin
625 .as_ref()
626 .and_then(|margin| Decimal::try_from(*margin).ok())
627 .unwrap_or(Decimal::ZERO);
628 let margin_maint = definition
629 .maint_margin
630 .as_ref()
631 .and_then(|margin| Decimal::try_from(*margin).ok())
632 .unwrap_or(Decimal::ZERO);
633
634 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
635 let max_quantity = convert_contract_quantity(
636 definition.max_order_qty,
637 contract_decimal,
638 max_scale,
639 "max quantity",
640 )?;
641 let min_quantity = lot_size;
642 let max_notional: Option<Money> = None;
643 let min_notional: Option<Money> = None;
644 let max_price = definition
645 .max_price
646 .map(|price| Price::from(price.to_string()));
647 let min_price = definition
648 .min_price
649 .map(|price| Price::from(price.to_string()));
650
651 let instrument = CryptoFuturesSpread::builder()
652 .instrument_id(instrument_id)
653 .raw_symbol(raw_symbol)
654 .underlying(underlying)
655 .quote_currency(quote_currency)
656 .settlement_currency(settlement_currency)
657 .is_inverse(is_inverse)
658 .strategy_type(Ustr::from("FS"))
659 .activation_ns(activation_ns)
660 .expiration_ns(expiration_ns)
661 .price_precision(price_increment.precision)
662 .size_precision(size_increment.precision)
663 .price_increment(price_increment)
664 .size_increment(size_increment)
665 .maybe_multiplier(multiplier)
666 .maybe_lot_size(lot_size)
667 .maybe_max_quantity(max_quantity)
668 .maybe_min_quantity(min_quantity)
669 .maybe_max_notional(max_notional)
670 .maybe_min_notional(min_notional)
671 .maybe_max_price(max_price)
672 .maybe_min_price(min_price)
673 .margin_init(margin_init)
674 .margin_maint(margin_maint)
675 .maker_fee(maker_fee)
676 .taker_fee(taker_fee)
677 .ts_event(ts_event)
678 .ts_init(ts_init)
679 .build()
680 .unwrap();
681
682 Ok(InstrumentAny::CryptoFuturesSpread(instrument))
683}
684
685pub fn parse_trade(
692 trade: &BitmexTrade,
693 instrument: &InstrumentAny,
694 ts_init: UnixNanos,
695) -> anyhow::Result<TradeTick> {
696 let instrument_id = parse_instrument_id(trade.symbol);
697 let price = Price::new(trade.price, instrument.price_precision());
698 let size = parse_contracts_quantity(trade.size as u64, instrument);
699 let aggressor_side = parse_aggressor_side(&trade.side);
700 let ts_event = UnixNanos::from(trade.timestamp);
701 let trade_id = match trade.trd_match_id {
702 Some(uuid) => TradeId::new(uuid.to_string()),
703 None => derive_trade_id(
704 trade.symbol,
705 ts_event.as_u64(),
706 trade.price,
707 trade.size,
708 trade.side,
709 ),
710 };
711
712 Ok(TradeTick::new(
713 instrument_id,
714 price,
715 size,
716 aggressor_side,
717 trade_id,
718 ts_event,
719 ts_init,
720 ))
721}
722
723pub fn parse_trade_bin(
729 bin: &BitmexTradeBin,
730 instrument: &InstrumentAny,
731 bar_type: &BarType,
732 ts_init: UnixNanos,
733) -> anyhow::Result<Bar> {
734 let instrument_id = bar_type.instrument_id();
735 let price_precision = instrument.price_precision();
736
737 let open = bin
738 .open
739 .ok_or_else(|| anyhow::anyhow!("Trade bin missing open price for {instrument_id}"))?;
740 let high = bin
741 .high
742 .ok_or_else(|| anyhow::anyhow!("Trade bin missing high price for {instrument_id}"))?;
743 let low = bin
744 .low
745 .ok_or_else(|| anyhow::anyhow!("Trade bin missing low price for {instrument_id}"))?;
746 let close = bin
747 .close
748 .ok_or_else(|| anyhow::anyhow!("Trade bin missing close price for {instrument_id}"))?;
749
750 let open = Price::new(open, price_precision);
751 let high = Price::new(high, price_precision);
752 let low = Price::new(low, price_precision);
753 let close = Price::new(close, price_precision);
754
755 let (open, high, low, close) =
756 normalize_trade_bin_prices(open, high, low, close, &bin.symbol, Some(bar_type));
757
758 let volume_contracts = normalize_trade_bin_volume(bin.volume, &bin.symbol);
759 let volume = parse_contracts_quantity(volume_contracts, instrument);
760 let ts_event = UnixNanos::from(bin.timestamp);
761
762 Ok(Bar::new(
763 *bar_type, open, high, low, close, volume, ts_event, ts_init,
764 ))
765}
766
767pub fn parse_order_status_report(
783 order: &BitmexOrder,
784 instrument: &InstrumentAny,
785 order_type_cache: &DashMap<ClientOrderId, OrderType>,
786 ts_init: UnixNanos,
787) -> anyhow::Result<OrderStatusReport> {
788 let instrument_id = instrument.id();
789 let account_id = bitmex_account_id(order.account);
790 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
791 let order_side = order.side.map(OrderSide::from);
792
793 let order_type: OrderType = order.ord_type.map_or_else(
796 || {
797 if let Some(cl_ord_id) = &order.cl_ord_id {
798 let client_order_id = ClientOrderId::new(cl_ord_id);
799 if let Some(cached_type) = order_type_cache.get(&client_order_id) {
800 log::debug!(
801 "Using cached ord_type={:?} for order {}",
802 *cached_type,
803 order.order_id,
804 );
805 return *cached_type;
806 }
807 }
808
809 let inferred = if order.stop_px.is_some() {
810 if order.price.is_some() {
811 OrderType::StopLimit
812 } else {
813 OrderType::StopMarket
814 }
815 } else if order.price.is_some() {
816 OrderType::Limit
817 } else {
818 OrderType::Market
819 };
820 log::debug!(
821 "Inferred ord_type={inferred:?} for order {} (price={:?}, stop_px={:?})",
822 order.order_id,
823 order.price,
824 order.stop_px,
825 );
826 inferred
827 },
828 |t| {
829 if t == BitmexOrderType::Pegged
831 && order.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
832 {
833 if order.price.is_some() {
834 OrderType::TrailingStopLimit
835 } else {
836 OrderType::TrailingStopMarket
837 }
838 } else {
839 t.into()
840 }
841 },
842 );
843
844 let time_in_force: TimeInForce = order
847 .time_in_force
848 .and_then(|tif| tif.try_into().ok())
849 .unwrap_or(TimeInForce::Gtc);
850
851 let order_status: OrderStatus = if let Some(status) = order.ord_status.as_ref() {
854 (*status).into()
855 } else {
856 match (order.leaves_qty, order.cum_qty, order.working_indicator) {
858 (Some(0), Some(cum), _) if cum > 0 => {
859 log::debug!(
860 "Inferred Filled from missing ordStatus (leaves_qty=0, cum_qty>0): order_id={:?}, client_order_id={:?}, cum_qty={}",
861 order.order_id,
862 order.cl_ord_id,
863 cum,
864 );
865 OrderStatus::Filled
866 }
867 (Some(0), _, _) => {
868 log::debug!(
869 "Inferred Canceled from missing ordStatus (leaves_qty=0, cum_qty<=0): order_id={:?}, client_order_id={:?}, cum_qty={:?}",
870 order.order_id,
871 order.cl_ord_id,
872 order.cum_qty,
873 );
874 OrderStatus::Canceled
875 }
876 (None, None, Some(false)) => {
878 log::debug!(
879 "Inferred Canceled from missing ordStatus with working_indicator=false: order_id={:?}, client_order_id={:?}",
880 order.order_id,
881 order.cl_ord_id,
882 );
883 OrderStatus::Canceled
884 }
885 _ => {
886 let order_json = serde_json::to_string(order)?;
887 anyhow::bail!(
888 "Order missing ord_status and cannot infer (order_id={}, client_order_id={:?}, leaves_qty={:?}, cum_qty={:?}, working_indicator={:?}, order_json={})",
889 order.order_id,
890 order.cl_ord_id,
891 order.leaves_qty,
892 order.cum_qty,
893 order.working_indicator,
894 order_json
895 );
896 }
897 }
898 };
899
900 let (quantity, filled_qty) = if let Some(qty) = order.order_qty {
902 let quantity = parse_signed_contracts_quantity(qty, instrument);
903 let filled_qty = parse_signed_contracts_quantity(order.cum_qty.unwrap_or(0), instrument);
904 (quantity, filled_qty)
905 } else if let (Some(cum), Some(leaves)) = (order.cum_qty, order.leaves_qty) {
906 log::debug!(
907 "Reconstructing order_qty from cum_qty + leaves_qty: order_id={:?}, client_order_id={:?}, cum_qty={}, leaves_qty={}",
908 order.order_id,
909 order.cl_ord_id,
910 cum,
911 leaves,
912 );
913 let quantity = parse_signed_contracts_quantity(cum + leaves, instrument);
914 let filled_qty = parse_signed_contracts_quantity(cum, instrument);
915 (quantity, filled_qty)
916 } else if order_status == OrderStatus::Canceled || order_status == OrderStatus::Rejected {
917 log::debug!(
920 "Order missing quantity fields, using 0 for both (will be reconciled from cache): order_id={:?}, client_order_id={:?}, status={:?}",
921 order.order_id,
922 order.cl_ord_id,
923 order_status,
924 );
925 let zero_qty = Quantity::zero(instrument.size_precision());
926 (zero_qty, zero_qty)
927 } else {
928 anyhow::bail!(
929 "Order missing order_qty and cannot reconstruct (order_id={}, cum_qty={:?}, leaves_qty={:?})",
930 order.order_id,
931 order.cum_qty,
932 order.leaves_qty
933 );
934 };
935 let report_id = UUID4::new();
936 let ts_accepted = order.transact_time.map_or(ts_init, UnixNanos::from);
937 let ts_last = order.timestamp.map_or(ts_init, UnixNanos::from);
938
939 let mut report = OrderStatusReport::new(
940 account_id,
941 instrument_id,
942 None, venue_order_id,
944 order_side,
945 order_type,
946 time_in_force,
947 order_status,
948 quantity,
949 filled_qty,
950 ts_accepted,
951 ts_last,
952 ts_init,
953 Some(report_id),
954 );
955
956 if let Some(cl_ord_id) = order.cl_ord_id {
957 report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
958 }
959
960 if let Some(cl_ord_link_id) = order.cl_ord_link_id {
961 report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
962 }
963
964 let price_precision = instrument.price_precision();
965
966 if let Some(price) = order.price {
967 report = report.with_price(Price::new(price, price_precision));
968 }
969
970 if let Some(avg_px) = order.avg_px {
971 report = report.with_avg_px(avg_px);
972 }
973
974 if let Some(trigger_price) = order.stop_px {
975 report = report
976 .with_trigger_price(Price::new(trigger_price, price_precision))
977 .with_trigger_type(extract_trigger_type(order.exec_inst.as_ref()));
978 }
979
980 if matches!(
982 order_type,
983 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
984 ) && let Some(peg_offset) = order.peg_offset_value
985 {
986 let trailing_offset = Decimal::try_from(peg_offset.abs())
987 .unwrap_or_else(|_| Decimal::new(peg_offset.abs() as i64, 0));
988 report = report
989 .with_trailing_offset(trailing_offset)
990 .with_trailing_offset_type(TrailingOffsetType::Price);
991
992 if order.stop_px.is_none() {
993 report = report.with_trigger_type(extract_trigger_type(order.exec_inst.as_ref()));
994 }
995 }
996
997 if let Some(exec_instructions) = &order.exec_inst {
998 for inst in exec_instructions {
999 match inst {
1000 BitmexExecInstruction::ParticipateDoNotInitiate => {
1001 report = report.with_post_only(true);
1002 }
1003 BitmexExecInstruction::ReduceOnly => report = report.with_reduce_only(true),
1004 BitmexExecInstruction::LastPrice
1005 | BitmexExecInstruction::Close
1006 | BitmexExecInstruction::MarkPrice
1007 | BitmexExecInstruction::IndexPrice
1008 | BitmexExecInstruction::AllOrNone
1009 | BitmexExecInstruction::Fixed
1010 | BitmexExecInstruction::Unknown => {}
1011 }
1012 }
1013 }
1014
1015 if let Some(contingency_type) = order.contingency_type.and_then(Into::into) {
1016 report = report.with_contingency_type(contingency_type);
1017 }
1018
1019 if matches!(
1020 report.contingency_type,
1021 Some(ContingencyType::Oco | ContingencyType::Oto | ContingencyType::Ouo)
1022 ) && report.order_list_id.is_none()
1023 {
1024 log::debug!(
1025 "BitMEX order missing clOrdLinkID for contingent order: order_id={}, client_order_id={:?}, contingency_type={:?}",
1026 order.order_id,
1027 report.client_order_id,
1028 report.contingency_type,
1029 );
1030 }
1031
1032 if order_status == OrderStatus::Rejected {
1034 if let Some(reason) = order.ord_rej_reason.or(order.text) {
1035 log::debug!(
1036 "Order rejected with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
1037 order.order_id,
1038 order.cl_ord_id,
1039 reason,
1040 );
1041 report = report.with_cancel_reason(clean_reason(reason.as_ref()));
1042 } else {
1043 log::debug!(
1044 "Order rejected without reason from BitMEX: order_id={:?}, client_order_id={:?}, ord_status={:?}, ord_rej_reason={:?}, text={:?}",
1045 order.order_id,
1046 order.cl_ord_id,
1047 order.ord_status,
1048 order.ord_rej_reason,
1049 order.text,
1050 );
1051 }
1052 } else if order_status == OrderStatus::Canceled
1053 && let Some(reason) = order.ord_rej_reason.or(order.text)
1054 {
1055 log::trace!(
1056 "Order canceled with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
1057 order.order_id,
1058 order.cl_ord_id,
1059 reason,
1060 );
1061 report = report.with_cancel_reason(clean_reason(reason.as_ref()));
1062 }
1063
1064 Ok(report)
1067}
1068
1069pub fn parse_fill_report(
1082 exec: &BitmexExecution,
1083 instrument: &InstrumentAny,
1084 ts_init: UnixNanos,
1085) -> anyhow::Result<FillReport> {
1086 if !matches!(exec.exec_type, BitmexExecType::Trade) {
1089 anyhow::bail!("Skipping non-trade execution: {:?}", exec.exec_type);
1090 }
1091
1092 let order_id = exec.order_id.ok_or_else(|| {
1094 anyhow::anyhow!("Skipping execution without order_id: {:?}", exec.exec_type)
1095 })?;
1096
1097 let account_id = bitmex_account_id(exec.account);
1098 let instrument_id = instrument.id();
1099 let venue_order_id = VenueOrderId::new(order_id.to_string());
1100 let trade_id = TradeId::new(
1102 exec.trd_match_id
1103 .or(Some(exec.exec_id))
1104 .ok_or_else(|| anyhow::anyhow!("Fill missing both trd_match_id and exec_id"))?
1105 .to_string(),
1106 );
1107 let Some(side) = exec.side else {
1109 anyhow::bail!("Skipping execution without side: {:?}", exec.exec_type);
1110 };
1111 let order_side = OrderSide::from(side);
1112 let last_qty = parse_signed_contracts_quantity(exec.last_qty, instrument);
1113 let last_px = Price::new(exec.last_px, instrument.price_precision());
1114
1115 let settlement_currency_str = exec.settl_currency.unwrap_or(Ustr::from("XBT")).as_str();
1117 let mapped_currency = map_bitmex_currency(settlement_currency_str);
1118 let currency = get_currency(&mapped_currency);
1119 let commission = Money::new(exec.commission.unwrap_or(0.0), currency);
1120 let liquidity_side = parse_liquidity_side(&exec.last_liquidity_ind);
1121 let client_order_id = exec.cl_ord_id.map(ClientOrderId::new);
1122 let venue_position_id = None; let ts_event = exec.transact_time.map_or(ts_init, UnixNanos::from);
1124
1125 Ok(FillReport::new(
1126 account_id,
1127 instrument_id,
1128 venue_order_id,
1129 trade_id,
1130 order_side,
1131 last_qty,
1132 last_px,
1133 commission,
1134 liquidity_side,
1135 client_order_id,
1136 venue_position_id,
1137 ts_event,
1138 ts_init,
1139 None,
1140 ))
1141}
1142
1143pub fn parse_position_report(
1150 position: &BitmexPosition,
1151 instrument: &InstrumentAny,
1152 ts_init: UnixNanos,
1153) -> anyhow::Result<PositionStatusReport> {
1154 let account_id = bitmex_account_id(position.account);
1155 let instrument_id = instrument.id();
1156 let position_side = parse_position_side(position.current_qty);
1157 let quantity = parse_signed_contracts_quantity(position.current_qty.unwrap_or(0), instrument);
1158 let venue_position_id = None; let avg_px_open = position
1160 .avg_entry_price
1161 .and_then(|p| Decimal::from_str(&p.to_string()).ok());
1162 let ts_last = parse_optional_datetime_to_unix_nanos(&position.timestamp, "timestamp");
1163
1164 Ok(PositionStatusReport::new(
1165 account_id,
1166 instrument_id,
1167 position_side,
1168 quantity,
1169 ts_last,
1170 ts_init,
1171 None, venue_position_id, avg_px_open, ))
1175}
1176
1177pub fn get_currency(code: &str) -> Currency {
1182 Currency::get_or_create_crypto(code)
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use std::str::FromStr;
1188
1189 use jiff::Timestamp;
1190 use nautilus_model::{
1191 data::{BarSpecification, BarType},
1192 enums::{AggregationSource, BarAggregation, LiquiditySide, PositionSide, PriceType},
1193 instruments::InstrumentAny,
1194 };
1195 use rstest::rstest;
1196 use rust_decimal::{Decimal, prelude::ToPrimitive};
1197 use uuid::Uuid;
1198
1199 use super::*;
1200 use crate::{
1201 common::{
1202 enums::{
1203 BitmexContingencyType, BitmexFairMethod, BitmexInstrumentState,
1204 BitmexInstrumentType, BitmexLiquidityIndicator, BitmexMarkMethod,
1205 BitmexOrderStatus, BitmexOrderType, BitmexSide, BitmexTickDirection,
1206 BitmexTimeInForce,
1207 },
1208 testing::load_test_json,
1209 },
1210 http::models::{
1211 BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTradeBin,
1212 BitmexWallet,
1213 },
1214 };
1215
1216 #[rstest]
1217 fn test_perp_instrument_deserialization() {
1218 let json_data = load_test_json("http_get_instrument_xbtusd.json");
1219 let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1220
1221 assert_eq!(instrument.symbol, "XBTUSD");
1222 assert_eq!(instrument.root_symbol, "XBT");
1223 assert_eq!(instrument.state, BitmexInstrumentState::Open);
1224 assert!(instrument.is_inverse);
1225 assert_eq!(instrument.maker_fee, Some(0.0005));
1226 assert_eq!(
1227 instrument.timestamp,
1228 "2024-11-24T23:33:19.034Z".parse::<Timestamp>().unwrap()
1229 );
1230 }
1231
1232 #[rstest]
1233 fn test_parse_instrument_any_skips_unknown_instrument_state() {
1234 let json_data = load_test_json("http_get_instrument_xbtusd.json");
1235 let mut instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1236 instrument.state = BitmexInstrumentState::Unknown;
1237
1238 let result = parse_instrument_any(&instrument, UnixNanos::default());
1239
1240 assert!(
1241 matches!(result, InstrumentParseResult::Inactive { .. }),
1242 "expected Inactive for unknown state, was {result:?}"
1243 );
1244 }
1245
1246 #[rstest]
1247 fn test_parse_instrument_any_parses_active_crypto_futures_spread() {
1248 let json_data = load_test_json("http_get_instrument_xbtm26_xbtu26_spread.json");
1249 let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1250
1251 let result = parse_instrument_any(&instrument, UnixNanos::default());
1252
1253 match result {
1254 InstrumentParseResult::Ok(instrument_any) => {
1255 let InstrumentAny::CryptoFuturesSpread(spread) = *instrument_any else {
1256 panic!("expected CryptoFuturesSpread variant");
1257 };
1258
1259 assert_eq!(
1260 instrument.instrument_type,
1261 BitmexInstrumentType::FuturesSpread
1262 );
1263 assert_eq!(spread.id.symbol.as_str(), "XBTM26-XBTU26");
1264 assert_eq!(spread.id.venue.as_str(), "BITMEX");
1265 assert_eq!(spread.raw_symbol.as_str(), "XBTM26-XBTU26");
1266 assert_eq!(spread.underlying.code.as_str(), "XBT");
1267 assert_eq!(spread.quote_currency.code.as_str(), "USD");
1268 assert_eq!(spread.settlement_currency.code.as_str(), "XBT");
1269 assert_eq!(spread.strategy_type.as_str(), "FS");
1270 assert!(!spread.is_inverse);
1271 assert_eq!(spread.price_precision, 1);
1272 assert_eq!(spread.size_precision, 0);
1273 assert_eq!(spread.price_increment.as_f64(), 0.5);
1274 assert_eq!(spread.size_increment.as_f64(), 1.0);
1275 assert_eq!(spread.lot_size.as_f64(), 100.0);
1276 assert_eq!(spread.min_quantity.unwrap().as_f64(), 100.0);
1277 assert_eq!(spread.max_quantity.unwrap().as_f64(), 10000000.0);
1278 assert_eq!(spread.min_price.unwrap().as_f64(), -1000000.0);
1279 assert_eq!(spread.max_price.unwrap().as_f64(), 1000000.0);
1280 assert_eq!(spread.maker_fee.to_f64().unwrap(), 0.0005);
1281 assert_eq!(spread.taker_fee.to_f64().unwrap(), 0.0005);
1282 assert!(spread.activation_ns.as_u64() > 0);
1283 assert!(spread.expiration_ns.as_u64() > 0);
1284 }
1285 result => panic!("expected parsed crypto futures spread, was {result:?}"),
1286 }
1287 }
1288
1289 #[rstest]
1290 fn test_parse_orders() {
1291 let json_data = load_test_json("http_get_orders.json");
1292 let orders: Vec<BitmexOrder> = serde_json::from_str(&json_data).unwrap();
1293
1294 assert_eq!(orders.len(), 2);
1295
1296 let order1 = &orders[0];
1298 assert_eq!(order1.symbol, Some(Ustr::from("XBTUSD")));
1299 assert_eq!(order1.side, Some(BitmexSide::Buy));
1300 assert_eq!(order1.order_qty, Some(100));
1301 assert_eq!(order1.price, Some(98000.0));
1302 assert_eq!(order1.ord_status, Some(BitmexOrderStatus::New));
1303 assert_eq!(order1.leaves_qty, Some(100));
1304 assert_eq!(order1.cum_qty, Some(0));
1305
1306 let order2 = &orders[1];
1308 assert_eq!(order2.symbol, Some(Ustr::from("XBTUSD")));
1309 assert_eq!(order2.side, Some(BitmexSide::Sell));
1310 assert_eq!(order2.order_qty, Some(200));
1311 assert_eq!(order2.ord_status, Some(BitmexOrderStatus::Filled));
1312 assert_eq!(order2.leaves_qty, Some(0));
1313 assert_eq!(order2.cum_qty, Some(200));
1314 assert_eq!(
1315 order2.avg_px,
1316 Some(Decimal::from_str("98950.500000000004").unwrap())
1317 );
1318 }
1319
1320 #[rstest]
1321 fn test_parse_executions() {
1322 let json_data = load_test_json("http_get_executions.json");
1323 let executions: Vec<BitmexExecution> = serde_json::from_str(&json_data).unwrap();
1324
1325 assert_eq!(executions.len(), 2);
1326
1327 let exec1 = &executions[0];
1329 assert_eq!(exec1.symbol, Some(Ustr::from("XBTUSD")));
1330 assert_eq!(exec1.side, Some(BitmexSide::Sell));
1331 assert_eq!(exec1.last_qty, 100);
1332 assert_eq!(exec1.last_px, 98950.0);
1333 assert_eq!(
1334 exec1.last_liquidity_ind,
1335 Some(BitmexLiquidityIndicator::Maker)
1336 );
1337 assert_eq!(exec1.commission, Some(0.00075));
1338
1339 let exec2 = &executions[1];
1341 assert_eq!(
1342 exec2.last_liquidity_ind,
1343 Some(BitmexLiquidityIndicator::Taker)
1344 );
1345 assert_eq!(exec2.last_px, 98951.0);
1346 }
1347
1348 #[rstest]
1349 fn test_parse_positions() {
1350 let json_data = load_test_json("http_get_positions.json");
1351 let positions: Vec<BitmexPosition> = serde_json::from_str(&json_data).unwrap();
1352
1353 assert_eq!(positions.len(), 1);
1354
1355 let position = &positions[0];
1356 assert_eq!(position.account, 1234567);
1357 assert_eq!(position.symbol, "XBTUSD");
1358 assert_eq!(position.current_qty, Some(100));
1359 assert_eq!(position.avg_entry_price, Some(98390.88));
1360 assert_eq!(position.unrealised_pnl, Some(1350));
1361 assert_eq!(position.realised_pnl, Some(-227));
1362 assert_eq!(position.is_open, Some(true));
1363 }
1364
1365 #[rstest]
1366 fn test_parse_trades() {
1367 let json_data = load_test_json("http_get_trades.json");
1368 let trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
1369
1370 assert_eq!(trades.len(), 3);
1371
1372 let trade1 = &trades[0];
1374 assert_eq!(trade1.symbol, "XBTUSD");
1375 assert_eq!(trade1.side, Some(BitmexSide::Buy));
1376 assert_eq!(trade1.size, 100);
1377 assert_eq!(trade1.price, 98950.0);
1378
1379 let trade3 = &trades[2];
1381 assert_eq!(trade3.side, Some(BitmexSide::Sell));
1382 assert_eq!(trade3.size, 50);
1383 assert_eq!(trade3.price, 98949.5);
1384 }
1385
1386 #[rstest]
1387 fn test_parse_trade_derives_trade_id_when_trd_match_id_missing() {
1388 let json_data = load_test_json("http_get_trades.json");
1389 let mut trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
1390 trades[0].trd_match_id = None;
1391 trades[1] = trades[0].clone();
1392 trades[2] = trades[0].clone();
1393 trades[2].price += 1.0;
1394
1395 let instrument =
1396 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1397 .unwrap();
1398
1399 let tick_a = parse_trade(&trades[0], &instrument, UnixNanos::from(1)).unwrap();
1400 let tick_b = parse_trade(&trades[1], &instrument, UnixNanos::from(1)).unwrap();
1401 let tick_c = parse_trade(&trades[2], &instrument, UnixNanos::from(1)).unwrap();
1402
1403 assert_eq!(
1404 tick_a.trade_id, tick_b.trade_id,
1405 "derivation must be stable"
1406 );
1407 assert_eq!(tick_a.trade_id.as_str().len(), 16);
1408 assert_ne!(
1409 tick_a.trade_id, tick_c.trade_id,
1410 "distinct price must distinguish"
1411 );
1412 }
1413
1414 #[rstest]
1415 fn test_parse_wallet() {
1416 let json_data = load_test_json("http_get_wallet.json");
1417 let wallets: Vec<BitmexWallet> = serde_json::from_str(&json_data).unwrap();
1418
1419 assert_eq!(wallets.len(), 1);
1420
1421 let wallet = &wallets[0];
1422 assert_eq!(wallet.account, 1234567);
1423 assert_eq!(wallet.currency, "XBt");
1424 assert_eq!(wallet.amount, Some(1000123456));
1425 assert_eq!(wallet.delta_amount, Some(123456));
1426 }
1427
1428 #[rstest]
1429 fn test_parse_trade_bins() {
1430 let json_data = load_test_json("http_get_trade_bins.json");
1431 let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1432
1433 assert_eq!(bins.len(), 3);
1434
1435 let bin1 = &bins[0];
1437 assert_eq!(bin1.symbol, "XBTUSD");
1438 assert_eq!(bin1.open, Some(98900.0));
1439 assert_eq!(bin1.high, Some(98980.5));
1440 assert_eq!(bin1.low, Some(98890.0));
1441 assert_eq!(bin1.close, Some(98950.0));
1442 assert_eq!(bin1.volume, Some(150000));
1443 assert_eq!(bin1.trades, Some(45));
1444
1445 let bin3 = &bins[2];
1447 assert_eq!(bin3.close, Some(98970.0));
1448 assert_eq!(bin3.volume, Some(78000));
1449 }
1450
1451 #[rstest]
1452 fn test_parse_trade_bin_to_bar() {
1453 let json_data = load_test_json("http_get_trade_bins.json");
1454 let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1455 let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1456 let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1457
1458 let ts_init = UnixNanos::from(1u64);
1459 let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1460 InstrumentParseResult::Ok(inst) => inst,
1461 other => panic!("Expected Ok, was {other:?}"),
1462 };
1463
1464 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1465 let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1466
1467 let bar = parse_trade_bin(&bins[0], &instrument_any, &bar_type, ts_init).unwrap();
1468
1469 let precision = instrument_any.price_precision();
1470 let expected_open =
1471 Price::from_decimal_dp(Decimal::from_str("98900.0").unwrap(), precision)
1472 .expect("open price");
1473 let expected_close =
1474 Price::from_decimal_dp(Decimal::from_str("98950.0").unwrap(), precision)
1475 .expect("close price");
1476
1477 assert_eq!(bar.bar_type, bar_type);
1478 assert_eq!(bar.open, expected_open);
1479 assert_eq!(bar.close, expected_close);
1480 }
1481
1482 #[rstest]
1483 fn test_parse_trade_bin_extreme_adjustment() {
1484 let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1485 let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1486
1487 let ts_init = UnixNanos::from(1u64);
1488 let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1489 InstrumentParseResult::Ok(inst) => inst,
1490 other => panic!("Expected Ok, was {other:?}"),
1491 };
1492
1493 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1494 let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1495
1496 let bin = BitmexTradeBin {
1497 timestamp: "2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap(),
1498 symbol: Ustr::from("XBTUSD"),
1499 open: Some(50_000.0),
1500 high: Some(49_990.0),
1501 low: Some(50_010.0),
1502 close: Some(50_005.0),
1503 trades: Some(5),
1504 volume: Some(1_000),
1505 vwap: None,
1506 last_size: None,
1507 turnover: None,
1508 home_notional: None,
1509 foreign_notional: None,
1510 };
1511
1512 let bar = parse_trade_bin(&bin, &instrument_any, &bar_type, ts_init).unwrap();
1513
1514 let precision = instrument_any.price_precision();
1515 let expected_high =
1516 Price::from_decimal_dp(Decimal::from_str("50010.0").unwrap(), precision)
1517 .expect("high price");
1518 let expected_low = Price::from_decimal_dp(Decimal::from_str("49990.0").unwrap(), precision)
1519 .expect("low price");
1520 let expected_open =
1521 Price::from_decimal_dp(Decimal::from_str("50000.0").unwrap(), precision)
1522 .expect("open price");
1523
1524 assert_eq!(bar.high, expected_high);
1525 assert_eq!(bar.low, expected_low);
1526 assert_eq!(bar.open, expected_open);
1527 }
1528
1529 #[rstest]
1530 fn test_parse_order_status_report() {
1531 let order = BitmexOrder {
1532 account: 123456,
1533 symbol: Some(Ustr::from("XBTUSD")),
1534 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
1535 cl_ord_id: Some(Ustr::from("client-123")),
1536 cl_ord_link_id: None,
1537 side: Some(BitmexSide::Buy),
1538 ord_type: Some(BitmexOrderType::Limit),
1539 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1540 ord_status: Some(BitmexOrderStatus::New),
1541 order_qty: Some(100),
1542 cum_qty: Some(50),
1543 price: Some(50000.0),
1544 stop_px: Some(49000.0),
1545 display_qty: None,
1546 peg_offset_value: None,
1547 peg_price_type: None,
1548 currency: Some(Ustr::from("USD")),
1549 settl_currency: Some(Ustr::from("XBt")),
1550 exec_inst: Some(vec![
1551 BitmexExecInstruction::ParticipateDoNotInitiate,
1552 BitmexExecInstruction::ReduceOnly,
1553 ]),
1554 contingency_type: Some(BitmexContingencyType::OneCancelsTheOther),
1555 ex_destination: None,
1556 triggered: None,
1557 working_indicator: Some(true),
1558 ord_rej_reason: None,
1559 leaves_qty: Some(50),
1560 avg_px: None,
1561 multi_leg_reporting_type: None,
1562 text: None,
1563 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1564 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1565 };
1566
1567 let instrument =
1568 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1569 .unwrap();
1570 let report =
1571 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1572 .unwrap();
1573
1574 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
1575 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1576 assert_eq!(
1577 report.venue_order_id.as_str(),
1578 "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
1579 );
1580 assert_eq!(report.client_order_id.unwrap().as_str(), "client-123");
1581 assert_eq!(report.quantity.as_f64(), 100.0);
1582 assert_eq!(report.filled_qty.as_f64(), 50.0);
1583 assert_eq!(report.price.unwrap().as_f64(), 50000.0);
1584 assert_eq!(report.trigger_price.unwrap().as_f64(), 49000.0);
1585 assert!(report.post_only);
1586 assert!(report.reduce_only);
1587 }
1588
1589 #[rstest]
1590 fn test_parse_order_status_report_minimal() {
1591 let order = BitmexOrder {
1592 account: 0, symbol: Some(Ustr::from("ETHUSD")),
1594 order_id: Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap(),
1595 cl_ord_id: None,
1596 cl_ord_link_id: None,
1597 side: Some(BitmexSide::Sell),
1598 ord_type: Some(BitmexOrderType::Market),
1599 time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1600 ord_status: Some(BitmexOrderStatus::Filled),
1601 order_qty: Some(200),
1602 cum_qty: Some(200),
1603 price: None,
1604 stop_px: None,
1605 display_qty: None,
1606 peg_offset_value: None,
1607 peg_price_type: None,
1608 currency: None,
1609 settl_currency: None,
1610 exec_inst: None,
1611 contingency_type: None,
1612 ex_destination: None,
1613 triggered: None,
1614 working_indicator: Some(false),
1615 ord_rej_reason: None,
1616 leaves_qty: Some(0),
1617 avg_px: None,
1618 multi_leg_reporting_type: None,
1619 text: None,
1620 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1621 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1622 };
1623
1624 let mut instrument_def = create_test_perpetual_instrument();
1625 instrument_def.symbol = Ustr::from("ETHUSD");
1626 instrument_def.underlying = Ustr::from("ETH");
1627 instrument_def.quote_currency = Ustr::from("USD");
1628 instrument_def.settl_currency = Some(Ustr::from("USDt"));
1629 let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
1630 let report =
1631 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1632 .unwrap();
1633
1634 assert_eq!(report.account_id.to_string(), "BITMEX-0");
1635 assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
1636 assert_eq!(
1637 report.venue_order_id.as_str(),
1638 "11111111-2222-3333-4444-555555555555"
1639 );
1640 assert!(report.client_order_id.is_none());
1641 assert_eq!(report.quantity.as_f64(), 200.0);
1642 assert_eq!(report.filled_qty.as_f64(), 200.0);
1643 assert!(report.price.is_none());
1644 assert!(report.trigger_price.is_none());
1645 assert!(!report.post_only);
1646 assert!(!report.reduce_only);
1647 }
1648
1649 #[rstest]
1650 fn test_parse_order_status_report_missing_order_qty_reconstructed() {
1651 let order = BitmexOrder {
1652 account: 789012,
1653 symbol: Some(Ustr::from("XBTUSD")),
1654 order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1655 cl_ord_id: Some(Ustr::from("client-cancel-test")),
1656 cl_ord_link_id: None,
1657 side: Some(BitmexSide::Buy),
1658 ord_type: Some(BitmexOrderType::Limit),
1659 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1660 ord_status: Some(BitmexOrderStatus::Canceled),
1661 order_qty: None, cum_qty: Some(75), leaves_qty: Some(25), price: Some(45000.0),
1665 stop_px: None,
1666 display_qty: None,
1667 peg_offset_value: None,
1668 peg_price_type: None,
1669 currency: Some(Ustr::from("USD")),
1670 settl_currency: Some(Ustr::from("XBt")),
1671 exec_inst: None,
1672 contingency_type: None,
1673 ex_destination: None,
1674 triggered: None,
1675 working_indicator: Some(false),
1676 ord_rej_reason: None,
1677 avg_px: Some(Decimal::from_str("45050.0").unwrap()),
1678 multi_leg_reporting_type: None,
1679 text: None,
1680 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1681 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1682 };
1683
1684 let instrument =
1685 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1686 .unwrap();
1687 let report =
1688 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1689 .unwrap();
1690
1691 assert_eq!(report.quantity.as_f64(), 100.0); assert_eq!(report.filled_qty.as_f64(), 75.0);
1694 assert_eq!(report.order_status, OrderStatus::Canceled);
1695 }
1696
1697 #[rstest]
1698 fn test_parse_order_status_report_uses_provided_order_qty() {
1699 let order = BitmexOrder {
1700 account: 123456,
1701 symbol: Some(Ustr::from("XBTUSD")),
1702 order_id: Uuid::parse_str("bbbbcccc-dddd-eeee-ffff-000000000000").unwrap(),
1703 cl_ord_id: Some(Ustr::from("client-provided-qty")),
1704 cl_ord_link_id: None,
1705 side: Some(BitmexSide::Sell),
1706 ord_type: Some(BitmexOrderType::Limit),
1707 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1708 ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1709 order_qty: Some(150), cum_qty: Some(50), leaves_qty: Some(100), price: Some(48000.0),
1713 stop_px: None,
1714 display_qty: None,
1715 peg_offset_value: None,
1716 peg_price_type: None,
1717 currency: Some(Ustr::from("USD")),
1718 settl_currency: Some(Ustr::from("XBt")),
1719 exec_inst: None,
1720 contingency_type: None,
1721 ex_destination: None,
1722 triggered: None,
1723 working_indicator: Some(true),
1724 ord_rej_reason: None,
1725 avg_px: Some(Decimal::from_str("48100.0").unwrap()),
1726 multi_leg_reporting_type: None,
1727 text: None,
1728 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1729 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1730 };
1731
1732 let instrument =
1733 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1734 .unwrap();
1735 let report =
1736 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1737 .unwrap();
1738
1739 assert_eq!(report.quantity.as_f64(), 150.0);
1741 assert_eq!(report.filled_qty.as_f64(), 50.0);
1742 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
1743 }
1744
1745 #[rstest]
1746 fn test_parse_order_status_report_missing_order_qty_fails() {
1747 let order = BitmexOrder {
1748 account: 789012,
1749 symbol: Some(Ustr::from("XBTUSD")),
1750 order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1751 cl_ord_id: Some(Ustr::from("client-fail-test")),
1752 cl_ord_link_id: None,
1753 side: Some(BitmexSide::Buy),
1754 ord_type: Some(BitmexOrderType::Limit),
1755 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1756 ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1757 order_qty: None, cum_qty: Some(75), leaves_qty: None, price: Some(45000.0),
1761 stop_px: None,
1762 display_qty: None,
1763 peg_offset_value: None,
1764 peg_price_type: None,
1765 currency: Some(Ustr::from("USD")),
1766 settl_currency: Some(Ustr::from("XBt")),
1767 exec_inst: None,
1768 contingency_type: None,
1769 ex_destination: None,
1770 triggered: None,
1771 working_indicator: Some(false),
1772 ord_rej_reason: None,
1773 avg_px: None,
1774 multi_leg_reporting_type: None,
1775 text: None,
1776 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1777 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1778 };
1779
1780 let instrument =
1781 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1782 .unwrap();
1783
1784 let result =
1786 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
1787 assert!(result.is_err());
1788 assert!(
1789 result
1790 .unwrap_err()
1791 .to_string()
1792 .contains("Order missing order_qty and cannot reconstruct")
1793 );
1794 }
1795
1796 #[rstest]
1797 fn test_parse_order_status_report_canceled_missing_all_quantities() {
1798 let order = BitmexOrder {
1799 account: 123456,
1800 symbol: Some(Ustr::from("XBTUSD")),
1801 order_id: Uuid::parse_str("ffff0000-1111-2222-3333-444444444444").unwrap(),
1802 cl_ord_id: Some(Ustr::from("client-cancel-no-qty")),
1803 cl_ord_link_id: None,
1804 side: Some(BitmexSide::Buy),
1805 ord_type: Some(BitmexOrderType::Limit),
1806 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1807 ord_status: Some(BitmexOrderStatus::Canceled),
1808 order_qty: None, cum_qty: None, leaves_qty: None, price: Some(50000.0),
1812 stop_px: None,
1813 display_qty: None,
1814 peg_offset_value: None,
1815 peg_price_type: None,
1816 currency: Some(Ustr::from("USD")),
1817 settl_currency: Some(Ustr::from("XBt")),
1818 exec_inst: None,
1819 contingency_type: None,
1820 ex_destination: None,
1821 triggered: None,
1822 working_indicator: Some(false),
1823 ord_rej_reason: None,
1824 avg_px: None,
1825 multi_leg_reporting_type: None,
1826 text: None,
1827 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1828 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1829 };
1830
1831 let instrument =
1832 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1833 .unwrap();
1834 let report =
1835 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1836 .unwrap();
1837
1838 assert_eq!(report.order_status, OrderStatus::Canceled);
1840 assert_eq!(report.quantity.as_f64(), 0.0);
1841 assert_eq!(report.filled_qty.as_f64(), 0.0);
1842 }
1843
1844 #[rstest]
1845 fn test_parse_order_status_report_rejected_with_reason() {
1846 let order = BitmexOrder {
1847 account: 123456,
1848 symbol: Some(Ustr::from("XBTUSD")),
1849 order_id: Uuid::parse_str("ccccdddd-eeee-ffff-0000-111111111111").unwrap(),
1850 cl_ord_id: Some(Ustr::from("client-rejected")),
1851 cl_ord_link_id: None,
1852 side: Some(BitmexSide::Buy),
1853 ord_type: Some(BitmexOrderType::Limit),
1854 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1855 ord_status: Some(BitmexOrderStatus::Rejected),
1856 order_qty: Some(100),
1857 cum_qty: Some(0),
1858 leaves_qty: Some(0),
1859 price: Some(50000.0),
1860 stop_px: None,
1861 display_qty: None,
1862 peg_offset_value: None,
1863 peg_price_type: None,
1864 currency: Some(Ustr::from("USD")),
1865 settl_currency: Some(Ustr::from("XBt")),
1866 exec_inst: None,
1867 contingency_type: None,
1868 ex_destination: None,
1869 triggered: None,
1870 working_indicator: Some(false),
1871 ord_rej_reason: Some(Ustr::from("Insufficient margin")),
1872 avg_px: None,
1873 multi_leg_reporting_type: None,
1874 text: None,
1875 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1876 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1877 };
1878
1879 let instrument =
1880 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1881 .unwrap();
1882 let report =
1883 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1884 .unwrap();
1885
1886 assert_eq!(report.order_status, OrderStatus::Rejected);
1887 assert_eq!(
1888 report.cancel_reason,
1889 Some("Insufficient margin".to_string())
1890 );
1891 }
1892
1893 #[rstest]
1894 fn test_parse_order_status_report_rejected_with_text_fallback() {
1895 let order = BitmexOrder {
1896 account: 123456,
1897 symbol: Some(Ustr::from("XBTUSD")),
1898 order_id: Uuid::parse_str("ddddeeee-ffff-0000-1111-222222222222").unwrap(),
1899 cl_ord_id: Some(Ustr::from("client-rejected-text")),
1900 cl_ord_link_id: None,
1901 side: Some(BitmexSide::Sell),
1902 ord_type: Some(BitmexOrderType::Limit),
1903 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1904 ord_status: Some(BitmexOrderStatus::Rejected),
1905 order_qty: Some(100),
1906 cum_qty: Some(0),
1907 leaves_qty: Some(0),
1908 price: Some(50000.0),
1909 stop_px: None,
1910 display_qty: None,
1911 peg_offset_value: None,
1912 peg_price_type: None,
1913 currency: Some(Ustr::from("USD")),
1914 settl_currency: Some(Ustr::from("XBt")),
1915 exec_inst: None,
1916 contingency_type: None,
1917 ex_destination: None,
1918 triggered: None,
1919 working_indicator: Some(false),
1920 ord_rej_reason: None,
1921 avg_px: None,
1922 multi_leg_reporting_type: None,
1923 text: Some(Ustr::from("Order would immediately execute")),
1924 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1925 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1926 };
1927
1928 let instrument =
1929 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1930 .unwrap();
1931 let report =
1932 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1933 .unwrap();
1934
1935 assert_eq!(report.order_status, OrderStatus::Rejected);
1936 assert_eq!(
1937 report.cancel_reason,
1938 Some("Order would immediately execute".to_string())
1939 );
1940 }
1941
1942 #[rstest]
1943 fn test_parse_order_status_report_rejected_without_reason() {
1944 let order = BitmexOrder {
1945 account: 123456,
1946 symbol: Some(Ustr::from("XBTUSD")),
1947 order_id: Uuid::parse_str("eeeeffff-0000-1111-2222-333333333333").unwrap(),
1948 cl_ord_id: Some(Ustr::from("client-rejected-no-reason")),
1949 cl_ord_link_id: None,
1950 side: Some(BitmexSide::Buy),
1951 ord_type: Some(BitmexOrderType::Market),
1952 time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1953 ord_status: Some(BitmexOrderStatus::Rejected),
1954 order_qty: Some(50),
1955 cum_qty: Some(0),
1956 leaves_qty: Some(0),
1957 price: None,
1958 stop_px: None,
1959 display_qty: None,
1960 peg_offset_value: None,
1961 peg_price_type: None,
1962 currency: Some(Ustr::from("USD")),
1963 settl_currency: Some(Ustr::from("XBt")),
1964 exec_inst: None,
1965 contingency_type: None,
1966 ex_destination: None,
1967 triggered: None,
1968 working_indicator: Some(false),
1969 ord_rej_reason: None,
1970 avg_px: None,
1971 multi_leg_reporting_type: None,
1972 text: None,
1973 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
1974 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
1975 };
1976
1977 let instrument =
1978 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1979 .unwrap();
1980 let report =
1981 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
1982 .unwrap();
1983
1984 assert_eq!(report.order_status, OrderStatus::Rejected);
1985 assert_eq!(report.cancel_reason, None);
1986 }
1987
1988 #[rstest]
1989 fn test_parse_fill_report() {
1990 let exec = BitmexExecution {
1991 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
1992 account: 654321,
1993 symbol: Some(Ustr::from("XBTUSD")),
1994 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
1995 cl_ord_id: Some(Ustr::from("client-456")),
1996 side: Some(BitmexSide::Buy),
1997 last_qty: 50,
1998 last_px: 50100.5,
1999 commission: Some(0.00075),
2000 settl_currency: Some(Ustr::from("XBt")),
2001 last_liquidity_ind: Some(BitmexLiquidityIndicator::Taker),
2002 trd_match_id: Some(Uuid::parse_str("99999999-8888-7777-6666-555555555555").unwrap()),
2003 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2004 cl_ord_link_id: None,
2005 underlying_last_px: None,
2006 last_mkt: None,
2007 order_qty: Some(50),
2008 price: Some(50100.0),
2009 display_qty: None,
2010 stop_px: None,
2011 peg_offset_value: None,
2012 peg_price_type: None,
2013 currency: None,
2014 exec_type: BitmexExecType::Trade,
2015 ord_type: BitmexOrderType::Limit,
2016 time_in_force: BitmexTimeInForce::GoodTillCancel,
2017 exec_inst: None,
2018 contingency_type: None,
2019 ex_destination: None,
2020 ord_status: Some(BitmexOrderStatus::Filled),
2021 triggered: None,
2022 working_indicator: None,
2023 ord_rej_reason: None,
2024 leaves_qty: None,
2025 cum_qty: Some(50),
2026 avg_px: Some(50100.5),
2027 trade_publish_indicator: None,
2028 multi_leg_reporting_type: None,
2029 text: None,
2030 exec_cost: None,
2031 exec_comm: None,
2032 home_notional: None,
2033 foreign_notional: None,
2034 timestamp: None,
2035 };
2036
2037 let instrument =
2038 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2039 .unwrap();
2040
2041 let report = parse_fill_report(&exec, &instrument, UnixNanos::from(1)).unwrap();
2042
2043 assert_eq!(report.account_id.to_string(), "BITMEX-654321");
2044 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
2045 assert_eq!(
2046 report.venue_order_id.as_str(),
2047 "a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6"
2048 );
2049 assert_eq!(
2050 report.trade_id.to_string(),
2051 "99999999-8888-7777-6666-555555555555"
2052 );
2053 assert_eq!(report.client_order_id.unwrap().as_str(), "client-456");
2054 assert_eq!(report.last_qty.as_f64(), 50.0);
2055 assert_eq!(report.last_px.as_f64(), 50100.5);
2056 assert_eq!(report.commission.as_f64(), 0.00075);
2057 assert_eq!(report.commission.currency.code.as_str(), "XBT");
2058 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
2059 }
2060
2061 #[rstest]
2062 fn test_parse_fill_report_with_missing_trd_match_id() {
2063 let exec = BitmexExecution {
2064 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
2065 account: 111111,
2066 symbol: Some(Ustr::from("ETHUSD")),
2067 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
2068 cl_ord_id: None,
2069 side: Some(BitmexSide::Sell),
2070 last_qty: 100,
2071 last_px: 3000.0,
2072 commission: None,
2073 settl_currency: None,
2074 last_liquidity_ind: Some(BitmexLiquidityIndicator::Maker),
2075 trd_match_id: None, transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2077 cl_ord_link_id: None,
2078 underlying_last_px: None,
2079 last_mkt: None,
2080 order_qty: Some(100),
2081 price: Some(3000.0),
2082 display_qty: None,
2083 stop_px: None,
2084 peg_offset_value: None,
2085 peg_price_type: None,
2086 currency: None,
2087 exec_type: BitmexExecType::Trade,
2088 ord_type: BitmexOrderType::Market,
2089 time_in_force: BitmexTimeInForce::ImmediateOrCancel,
2090 exec_inst: None,
2091 contingency_type: None,
2092 ex_destination: None,
2093 ord_status: Some(BitmexOrderStatus::Filled),
2094 triggered: None,
2095 working_indicator: None,
2096 ord_rej_reason: None,
2097 leaves_qty: None,
2098 cum_qty: Some(100),
2099 avg_px: Some(3000.0),
2100 trade_publish_indicator: None,
2101 multi_leg_reporting_type: None,
2102 text: None,
2103 exec_cost: None,
2104 exec_comm: None,
2105 home_notional: None,
2106 foreign_notional: None,
2107 timestamp: None,
2108 };
2109
2110 let mut instrument_def = create_test_perpetual_instrument();
2111 instrument_def.symbol = Ustr::from("ETHUSD");
2112 instrument_def.underlying = Ustr::from("ETH");
2113 instrument_def.quote_currency = Ustr::from("USD");
2114 instrument_def.settl_currency = Some(Ustr::from("USDt"));
2115 let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
2116
2117 let report = parse_fill_report(&exec, &instrument, UnixNanos::from(1)).unwrap();
2118
2119 assert_eq!(report.account_id.to_string(), "BITMEX-111111");
2120 assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
2121 assert_eq!(
2122 report.trade_id.to_string(),
2123 "f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6"
2124 );
2125 assert!(report.client_order_id.is_none());
2126 assert_eq!(report.commission.as_f64(), 0.0);
2127 assert_eq!(report.commission.currency.code.as_str(), "XBT");
2128 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
2129 }
2130
2131 #[rstest]
2132 fn test_parse_position_report() {
2133 let position = BitmexPosition {
2134 account: 789012,
2135 symbol: Ustr::from("XBTUSD"),
2136 current_qty: Some(1000),
2137 timestamp: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2138 currency: None,
2139 underlying: None,
2140 quote_currency: None,
2141 commission: None,
2142 init_margin_req: None,
2143 maint_margin_req: None,
2144 risk_limit: None,
2145 leverage: None,
2146 cross_margin: None,
2147 deleverage_percentile: None,
2148 rebalanced_pnl: None,
2149 prev_realised_pnl: None,
2150 prev_unrealised_pnl: None,
2151 prev_close_price: None,
2152 opening_timestamp: None,
2153 opening_qty: None,
2154 opening_cost: None,
2155 opening_comm: None,
2156 open_order_buy_qty: None,
2157 open_order_buy_cost: None,
2158 open_order_buy_premium: None,
2159 open_order_sell_qty: None,
2160 open_order_sell_cost: None,
2161 open_order_sell_premium: None,
2162 exec_buy_qty: None,
2163 exec_buy_cost: None,
2164 exec_sell_qty: None,
2165 exec_sell_cost: None,
2166 exec_qty: None,
2167 exec_cost: None,
2168 exec_comm: None,
2169 current_timestamp: None,
2170 current_cost: None,
2171 current_comm: None,
2172 realised_cost: None,
2173 unrealised_cost: None,
2174 gross_open_cost: None,
2175 gross_open_premium: None,
2176 gross_exec_cost: None,
2177 is_open: Some(true),
2178 mark_price: None,
2179 mark_value: None,
2180 risk_value: None,
2181 home_notional: None,
2182 foreign_notional: None,
2183 pos_state: None,
2184 pos_cost: None,
2185 pos_cost2: None,
2186 pos_cross: None,
2187 pos_init: None,
2188 pos_comm: None,
2189 pos_loss: None,
2190 pos_margin: None,
2191 pos_maint: None,
2192 pos_allowance: None,
2193 taxable_margin: None,
2194 init_margin: None,
2195 maint_margin: None,
2196 session_margin: None,
2197 target_excess_margin: None,
2198 var_margin: None,
2199 realised_gross_pnl: None,
2200 realised_tax: None,
2201 realised_pnl: None,
2202 unrealised_gross_pnl: None,
2203 long_bankrupt: None,
2204 short_bankrupt: None,
2205 tax_base: None,
2206 indicative_tax_rate: None,
2207 indicative_tax: None,
2208 unrealised_tax: None,
2209 unrealised_pnl: None,
2210 unrealised_pnl_pcnt: None,
2211 unrealised_roe_pcnt: None,
2212 avg_cost_price: None,
2213 avg_entry_price: None,
2214 break_even_price: None,
2215 margin_call_price: None,
2216 liquidation_price: None,
2217 bankrupt_price: None,
2218 last_price: None,
2219 last_value: None,
2220 };
2221
2222 let instrument =
2223 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2224 .unwrap();
2225
2226 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2227
2228 assert_eq!(report.account_id.to_string(), "BITMEX-789012");
2229 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
2230 assert_eq!(report.position_side, PositionSide::Long);
2231 assert_eq!(report.quantity.as_f64(), 1000.0);
2232 }
2233
2234 #[rstest]
2235 fn test_parse_position_report_short() {
2236 let position = BitmexPosition {
2237 account: 789012,
2238 symbol: Ustr::from("ETHUSD"),
2239 current_qty: Some(-500),
2240 timestamp: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2241 currency: None,
2242 underlying: None,
2243 quote_currency: None,
2244 commission: None,
2245 init_margin_req: None,
2246 maint_margin_req: None,
2247 risk_limit: None,
2248 leverage: None,
2249 cross_margin: None,
2250 deleverage_percentile: None,
2251 rebalanced_pnl: None,
2252 prev_realised_pnl: None,
2253 prev_unrealised_pnl: None,
2254 prev_close_price: None,
2255 opening_timestamp: None,
2256 opening_qty: None,
2257 opening_cost: None,
2258 opening_comm: None,
2259 open_order_buy_qty: None,
2260 open_order_buy_cost: None,
2261 open_order_buy_premium: None,
2262 open_order_sell_qty: None,
2263 open_order_sell_cost: None,
2264 open_order_sell_premium: None,
2265 exec_buy_qty: None,
2266 exec_buy_cost: None,
2267 exec_sell_qty: None,
2268 exec_sell_cost: None,
2269 exec_qty: None,
2270 exec_cost: None,
2271 exec_comm: None,
2272 current_timestamp: None,
2273 current_cost: None,
2274 current_comm: None,
2275 realised_cost: None,
2276 unrealised_cost: None,
2277 gross_open_cost: None,
2278 gross_open_premium: None,
2279 gross_exec_cost: None,
2280 is_open: Some(true),
2281 mark_price: None,
2282 mark_value: None,
2283 risk_value: None,
2284 home_notional: None,
2285 foreign_notional: None,
2286 pos_state: None,
2287 pos_cost: None,
2288 pos_cost2: None,
2289 pos_cross: None,
2290 pos_init: None,
2291 pos_comm: None,
2292 pos_loss: None,
2293 pos_margin: None,
2294 pos_maint: None,
2295 pos_allowance: None,
2296 taxable_margin: None,
2297 init_margin: None,
2298 maint_margin: None,
2299 session_margin: None,
2300 target_excess_margin: None,
2301 var_margin: None,
2302 realised_gross_pnl: None,
2303 realised_tax: None,
2304 realised_pnl: None,
2305 unrealised_gross_pnl: None,
2306 long_bankrupt: None,
2307 short_bankrupt: None,
2308 tax_base: None,
2309 indicative_tax_rate: None,
2310 indicative_tax: None,
2311 unrealised_tax: None,
2312 unrealised_pnl: None,
2313 unrealised_pnl_pcnt: None,
2314 unrealised_roe_pcnt: None,
2315 avg_cost_price: None,
2316 avg_entry_price: None,
2317 break_even_price: None,
2318 margin_call_price: None,
2319 liquidation_price: None,
2320 bankrupt_price: None,
2321 last_price: None,
2322 last_value: None,
2323 };
2324
2325 let mut instrument_def = create_test_futures_instrument();
2326 instrument_def.symbol = Ustr::from("ETHUSD");
2327 instrument_def.underlying = Ustr::from("ETH");
2328 instrument_def.quote_currency = Ustr::from("USD");
2329 instrument_def.settl_currency = Some(Ustr::from("USD"));
2330 let instrument = parse_futures_instrument(&instrument_def, UnixNanos::default()).unwrap();
2331
2332 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2333
2334 assert_eq!(report.position_side, PositionSide::Short);
2335 assert_eq!(report.quantity.as_f64(), 500.0); }
2337
2338 #[rstest]
2339 fn test_parse_position_report_flat() {
2340 let position = BitmexPosition {
2341 account: 789012,
2342 symbol: Ustr::from("SOLUSD"),
2343 current_qty: Some(0),
2344 timestamp: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2345 currency: None,
2346 underlying: None,
2347 quote_currency: None,
2348 commission: None,
2349 init_margin_req: None,
2350 maint_margin_req: None,
2351 risk_limit: None,
2352 leverage: None,
2353 cross_margin: None,
2354 deleverage_percentile: None,
2355 rebalanced_pnl: None,
2356 prev_realised_pnl: None,
2357 prev_unrealised_pnl: None,
2358 prev_close_price: None,
2359 opening_timestamp: None,
2360 opening_qty: None,
2361 opening_cost: None,
2362 opening_comm: None,
2363 open_order_buy_qty: None,
2364 open_order_buy_cost: None,
2365 open_order_buy_premium: None,
2366 open_order_sell_qty: None,
2367 open_order_sell_cost: None,
2368 open_order_sell_premium: None,
2369 exec_buy_qty: None,
2370 exec_buy_cost: None,
2371 exec_sell_qty: None,
2372 exec_sell_cost: None,
2373 exec_qty: None,
2374 exec_cost: None,
2375 exec_comm: None,
2376 current_timestamp: None,
2377 current_cost: None,
2378 current_comm: None,
2379 realised_cost: None,
2380 unrealised_cost: None,
2381 gross_open_cost: None,
2382 gross_open_premium: None,
2383 gross_exec_cost: None,
2384 is_open: Some(true),
2385 mark_price: None,
2386 mark_value: None,
2387 risk_value: None,
2388 home_notional: None,
2389 foreign_notional: None,
2390 pos_state: None,
2391 pos_cost: None,
2392 pos_cost2: None,
2393 pos_cross: None,
2394 pos_init: None,
2395 pos_comm: None,
2396 pos_loss: None,
2397 pos_margin: None,
2398 pos_maint: None,
2399 pos_allowance: None,
2400 taxable_margin: None,
2401 init_margin: None,
2402 maint_margin: None,
2403 session_margin: None,
2404 target_excess_margin: None,
2405 var_margin: None,
2406 realised_gross_pnl: None,
2407 realised_tax: None,
2408 realised_pnl: None,
2409 unrealised_gross_pnl: None,
2410 long_bankrupt: None,
2411 short_bankrupt: None,
2412 tax_base: None,
2413 indicative_tax_rate: None,
2414 indicative_tax: None,
2415 unrealised_tax: None,
2416 unrealised_pnl: None,
2417 unrealised_pnl_pcnt: None,
2418 unrealised_roe_pcnt: None,
2419 avg_cost_price: None,
2420 avg_entry_price: None,
2421 break_even_price: None,
2422 margin_call_price: None,
2423 liquidation_price: None,
2424 bankrupt_price: None,
2425 last_price: None,
2426 last_value: None,
2427 };
2428
2429 let mut instrument_def = create_test_spot_instrument();
2430 instrument_def.symbol = Ustr::from("SOLUSD");
2431 instrument_def.underlying = Ustr::from("SOL");
2432 instrument_def.quote_currency = Ustr::from("USD");
2433 let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2434
2435 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2436
2437 assert_eq!(report.position_side, PositionSide::Flat);
2438 assert_eq!(report.quantity.as_f64(), 0.0);
2439 }
2440
2441 #[rstest]
2442 fn test_parse_position_report_spot_scaling() {
2443 let position = BitmexPosition {
2444 account: 789012,
2445 symbol: Ustr::from("SOLUSD"),
2446 current_qty: Some(1000),
2447 timestamp: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
2448 currency: None,
2449 underlying: None,
2450 quote_currency: None,
2451 commission: None,
2452 init_margin_req: None,
2453 maint_margin_req: None,
2454 risk_limit: None,
2455 leverage: None,
2456 cross_margin: None,
2457 deleverage_percentile: None,
2458 rebalanced_pnl: None,
2459 prev_realised_pnl: None,
2460 prev_unrealised_pnl: None,
2461 prev_close_price: None,
2462 opening_timestamp: None,
2463 opening_qty: None,
2464 opening_cost: None,
2465 opening_comm: None,
2466 open_order_buy_qty: None,
2467 open_order_buy_cost: None,
2468 open_order_buy_premium: None,
2469 open_order_sell_qty: None,
2470 open_order_sell_cost: None,
2471 open_order_sell_premium: None,
2472 exec_buy_qty: None,
2473 exec_buy_cost: None,
2474 exec_sell_qty: None,
2475 exec_sell_cost: None,
2476 exec_qty: None,
2477 exec_cost: None,
2478 exec_comm: None,
2479 current_timestamp: None,
2480 current_cost: None,
2481 current_comm: None,
2482 realised_cost: None,
2483 unrealised_cost: None,
2484 gross_open_cost: None,
2485 gross_open_premium: None,
2486 gross_exec_cost: None,
2487 is_open: Some(true),
2488 mark_price: None,
2489 mark_value: None,
2490 risk_value: None,
2491 home_notional: None,
2492 foreign_notional: None,
2493 pos_state: None,
2494 pos_cost: None,
2495 pos_cost2: None,
2496 pos_cross: None,
2497 pos_init: None,
2498 pos_comm: None,
2499 pos_loss: None,
2500 pos_margin: None,
2501 pos_maint: None,
2502 pos_allowance: None,
2503 taxable_margin: None,
2504 init_margin: None,
2505 maint_margin: None,
2506 session_margin: None,
2507 target_excess_margin: None,
2508 var_margin: None,
2509 realised_gross_pnl: None,
2510 realised_tax: None,
2511 realised_pnl: None,
2512 unrealised_gross_pnl: None,
2513 long_bankrupt: None,
2514 short_bankrupt: None,
2515 tax_base: None,
2516 indicative_tax_rate: None,
2517 indicative_tax: None,
2518 unrealised_tax: None,
2519 unrealised_pnl: None,
2520 unrealised_pnl_pcnt: None,
2521 unrealised_roe_pcnt: None,
2522 avg_cost_price: None,
2523 avg_entry_price: None,
2524 break_even_price: None,
2525 margin_call_price: None,
2526 liquidation_price: None,
2527 bankrupt_price: None,
2528 last_price: None,
2529 last_value: None,
2530 };
2531
2532 let mut instrument_def = create_test_spot_instrument();
2533 instrument_def.symbol = Ustr::from("SOLUSD");
2534 instrument_def.underlying = Ustr::from("SOL");
2535 instrument_def.quote_currency = Ustr::from("USD");
2536 let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2537
2538 let report = parse_position_report(&position, &instrument, UnixNanos::from(1)).unwrap();
2539
2540 assert_eq!(report.position_side, PositionSide::Long);
2541 assert!((report.quantity.as_f64() - 0.1).abs() < 1e-9);
2542 }
2543
2544 fn create_test_spot_instrument() -> BitmexInstrument {
2545 BitmexInstrument {
2546 symbol: Ustr::from("XBTUSD"),
2547 root_symbol: Ustr::from("XBT"),
2548 state: BitmexInstrumentState::Open,
2549 instrument_type: BitmexInstrumentType::Spot,
2550 listing: Some("2016-05-13T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2551 front: Some("2016-05-13T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2552 expiry: None,
2553 settle: None,
2554 listed_settle: None,
2555 position_currency: Some(Ustr::from("USD")),
2556 underlying: Ustr::from("XBT"),
2557 quote_currency: Ustr::from("USD"),
2558 underlying_symbol: Some(Ustr::from("XBT=")),
2559 reference: Some(Ustr::from("BMEX")),
2560 reference_symbol: Some(Ustr::from(".BXBT")),
2561 lot_size: Some(1000.0),
2562 tick_size: 0.01,
2563 multiplier: 1.0,
2564 settl_currency: Some(Ustr::from("USD")),
2565 is_quanto: false,
2566 is_inverse: false,
2567 maker_fee: Some(-0.00025),
2568 taker_fee: Some(0.00075),
2569 timestamp: "2024-01-01T00:00:00.000Z".parse::<Timestamp>().unwrap(),
2570 max_order_qty: Some(10000000.0),
2572 max_price: Some(1000000.0),
2573 min_price: None,
2574 settlement_fee: Some(0.0),
2575 mark_price: Some(50500.0),
2576 last_price: Some(50500.0),
2577 bid_price: Some(50499.5),
2578 ask_price: Some(50500.5),
2579 open_interest: Some(0.0),
2580 open_value: Some(0.0),
2581 total_volume: Some(1000000.0),
2582 volume: Some(50000.0),
2583 volume_24h: Some(75000.0),
2584 total_turnover: Some(150000000.0),
2585 turnover: Some(5000000.0),
2586 turnover_24h: Some(7500000.0),
2587 has_liquidity: Some(true),
2588 calc_interval: None,
2590 publish_interval: None,
2591 publish_time: None,
2592 underlying_to_position_multiplier: Some(10000.0),
2593 underlying_to_settle_multiplier: None,
2594 quote_to_settle_multiplier: Some(1.0),
2595 init_margin: Some(0.1),
2596 maint_margin: Some(0.05),
2597 risk_limit: Some(20000000000.0),
2598 risk_step: Some(10000000000.0),
2599 limit: None,
2600 taxed: Some(true),
2601 deleverage: Some(true),
2602 funding_base_symbol: None,
2603 funding_quote_symbol: None,
2604 funding_premium_symbol: None,
2605 funding_timestamp: None,
2606 funding_interval: None,
2607 funding_rate: None,
2608 indicative_funding_rate: None,
2609 rebalance_timestamp: None,
2610 rebalance_interval: None,
2611 prev_close_price: Some(50000.0),
2612 limit_down_price: None,
2613 limit_up_price: None,
2614 prev_total_turnover: Some(100000000.0),
2615 home_notional_24h: Some(1.5),
2616 foreign_notional_24h: Some(75000.0),
2617 prev_price_24h: Some(49500.0),
2618 vwap: Some(50100.0),
2619 high_price: Some(51000.0),
2620 low_price: Some(49000.0),
2621 last_price_protected: Some(50500.0),
2622 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2623 last_change_pcnt: Some(0.0202),
2624 mid_price: Some(50500.0),
2625 impact_bid_price: Some(50490.0),
2626 impact_mid_price: Some(50495.0),
2627 impact_ask_price: Some(50500.0),
2628 fair_method: None,
2629 fair_basis_rate: None,
2630 fair_basis: None,
2631 fair_price: None,
2632 mark_method: Some(BitmexMarkMethod::LastPrice),
2633 indicative_settle_price: None,
2634 settled_price_adjustment_rate: None,
2635 settled_price: None,
2636 instant_pnl: false,
2637 min_tick: None,
2638 funding_base_rate: None,
2639 funding_quote_rate: None,
2640 capped: None,
2641 opening_timestamp: None,
2642 closing_timestamp: None,
2643 prev_total_volume: None,
2644 }
2645 }
2646
2647 fn create_test_perpetual_instrument() -> BitmexInstrument {
2648 BitmexInstrument {
2649 symbol: Ustr::from("XBTUSD"),
2650 root_symbol: Ustr::from("XBT"),
2651 state: BitmexInstrumentState::Open,
2652 instrument_type: BitmexInstrumentType::PerpetualContract,
2653 listing: Some("2016-05-13T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2654 front: Some("2016-05-13T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2655 expiry: None,
2656 settle: None,
2657 listed_settle: None,
2658 position_currency: Some(Ustr::from("USD")),
2659 underlying: Ustr::from("XBT"),
2660 quote_currency: Ustr::from("USD"),
2661 underlying_symbol: Some(Ustr::from("XBT=")),
2662 reference: Some(Ustr::from("BMEX")),
2663 reference_symbol: Some(Ustr::from(".BXBT")),
2664 lot_size: Some(100.0),
2665 tick_size: 0.5,
2666 multiplier: -100000000.0,
2667 settl_currency: Some(Ustr::from("XBt")),
2668 is_quanto: false,
2669 is_inverse: true,
2670 maker_fee: Some(-0.00025),
2671 taker_fee: Some(0.00075),
2672 timestamp: "2024-01-01T00:00:00.000Z".parse::<Timestamp>().unwrap(),
2673 max_order_qty: Some(10000000.0),
2675 max_price: Some(1000000.0),
2676 min_price: None,
2677 settlement_fee: Some(0.0),
2678 mark_price: Some(50500.01),
2679 last_price: Some(50500.0),
2680 bid_price: Some(50499.5),
2681 ask_price: Some(50500.5),
2682 open_interest: Some(500000000.0),
2683 open_value: Some(990099009900.0),
2684 total_volume: Some(12345678900000.0),
2685 volume: Some(5000000.0),
2686 volume_24h: Some(75000000.0),
2687 total_turnover: Some(150000000000000.0),
2688 turnover: Some(5000000000.0),
2689 turnover_24h: Some(7500000000.0),
2690 has_liquidity: Some(true),
2691 funding_base_symbol: Some(Ustr::from(".XBTBON8H")),
2693 funding_quote_symbol: Some(Ustr::from(".USDBON8H")),
2694 funding_premium_symbol: Some(Ustr::from(".XBTUSDPI8H")),
2695 funding_timestamp: Some("2024-01-01T08:00:00.000Z".parse::<Timestamp>().unwrap()),
2696 funding_interval: Some("2000-01-01T08:00:00.000Z".parse::<Timestamp>().unwrap()),
2697 funding_rate: Some(Decimal::from_str("0.0001").unwrap()),
2698 indicative_funding_rate: Some(Decimal::from_str("0.0001").unwrap()),
2699 funding_base_rate: Some(0.01),
2700 funding_quote_rate: Some(-0.01),
2701 calc_interval: None,
2703 publish_interval: None,
2704 publish_time: None,
2705 underlying_to_position_multiplier: None,
2706 underlying_to_settle_multiplier: Some(-100000000.0),
2707 quote_to_settle_multiplier: None,
2708 init_margin: Some(0.01),
2709 maint_margin: Some(0.005),
2710 risk_limit: Some(20000000000.0),
2711 risk_step: Some(10000000000.0),
2712 limit: None,
2713 taxed: Some(true),
2714 deleverage: Some(true),
2715 rebalance_timestamp: None,
2716 rebalance_interval: None,
2717 prev_close_price: Some(50000.0),
2718 limit_down_price: None,
2719 limit_up_price: None,
2720 prev_total_turnover: Some(100000000000000.0),
2721 home_notional_24h: Some(1500.0),
2722 foreign_notional_24h: Some(75000000.0),
2723 prev_price_24h: Some(49500.0),
2724 vwap: Some(50100.0),
2725 high_price: Some(51000.0),
2726 low_price: Some(49000.0),
2727 last_price_protected: Some(50500.0),
2728 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2729 last_change_pcnt: Some(0.0202),
2730 mid_price: Some(50500.0),
2731 impact_bid_price: Some(50490.0),
2732 impact_mid_price: Some(50495.0),
2733 impact_ask_price: Some(50500.0),
2734 fair_method: Some(BitmexFairMethod::FundingRate),
2735 fair_basis_rate: Some(0.1095),
2736 fair_basis: Some(0.01),
2737 fair_price: Some(50500.01),
2738 mark_method: Some(BitmexMarkMethod::FairPrice),
2739 indicative_settle_price: Some(50500.0),
2740 settled_price_adjustment_rate: None,
2741 settled_price: None,
2742 instant_pnl: false,
2743 min_tick: None,
2744 capped: None,
2745 opening_timestamp: None,
2746 closing_timestamp: None,
2747 prev_total_volume: None,
2748 }
2749 }
2750
2751 fn create_test_futures_instrument() -> BitmexInstrument {
2752 BitmexInstrument {
2753 symbol: Ustr::from("XBTH25"),
2754 root_symbol: Ustr::from("XBT"),
2755 state: BitmexInstrumentState::Open,
2756 instrument_type: BitmexInstrumentType::Futures,
2757 listing: Some("2024-09-27T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2758 front: Some("2024-12-27T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2759 expiry: Some("2025-03-28T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2760 settle: Some("2025-03-28T12:00:00.000Z".parse::<Timestamp>().unwrap()),
2761 listed_settle: None,
2762 position_currency: Some(Ustr::from("USD")),
2763 underlying: Ustr::from("XBT"),
2764 quote_currency: Ustr::from("USD"),
2765 underlying_symbol: Some(Ustr::from("XBT=")),
2766 reference: Some(Ustr::from("BMEX")),
2767 reference_symbol: Some(Ustr::from(".BXBT30M")),
2768 lot_size: Some(100.0),
2769 tick_size: 0.5,
2770 multiplier: -100000000.0,
2771 settl_currency: Some(Ustr::from("XBt")),
2772 is_quanto: false,
2773 is_inverse: true,
2774 maker_fee: Some(-0.00025),
2775 taker_fee: Some(0.00075),
2776 settlement_fee: Some(0.0005),
2777 timestamp: "2024-01-01T00:00:00.000Z".parse::<Timestamp>().unwrap(),
2778 max_order_qty: Some(10000000.0),
2780 max_price: Some(1000000.0),
2781 min_price: None,
2782 mark_price: Some(55500.0),
2783 last_price: Some(55500.0),
2784 bid_price: Some(55499.5),
2785 ask_price: Some(55500.5),
2786 open_interest: Some(50000000.0),
2787 open_value: Some(90090090090.0),
2788 total_volume: Some(1000000000.0),
2789 volume: Some(500000.0),
2790 volume_24h: Some(7500000.0),
2791 total_turnover: Some(15000000000000.0),
2792 turnover: Some(500000000.0),
2793 turnover_24h: Some(750000000.0),
2794 has_liquidity: Some(true),
2795 funding_base_symbol: None,
2797 funding_quote_symbol: None,
2798 funding_premium_symbol: None,
2799 funding_timestamp: None,
2800 funding_interval: None,
2801 funding_rate: None,
2802 indicative_funding_rate: None,
2803 funding_base_rate: None,
2804 funding_quote_rate: None,
2805 calc_interval: None,
2807 publish_interval: None,
2808 publish_time: None,
2809 underlying_to_position_multiplier: None,
2810 underlying_to_settle_multiplier: Some(-100000000.0),
2811 quote_to_settle_multiplier: None,
2812 init_margin: Some(0.02),
2813 maint_margin: Some(0.01),
2814 risk_limit: Some(20000000000.0),
2815 risk_step: Some(10000000000.0),
2816 limit: None,
2817 taxed: Some(true),
2818 deleverage: Some(true),
2819 rebalance_timestamp: None,
2820 rebalance_interval: None,
2821 prev_close_price: Some(55000.0),
2822 limit_down_price: None,
2823 limit_up_price: None,
2824 prev_total_turnover: Some(10000000000000.0),
2825 home_notional_24h: Some(150.0),
2826 foreign_notional_24h: Some(7500000.0),
2827 prev_price_24h: Some(54500.0),
2828 vwap: Some(55100.0),
2829 high_price: Some(56000.0),
2830 low_price: Some(54000.0),
2831 last_price_protected: Some(55500.0),
2832 last_tick_direction: Some(BitmexTickDirection::PlusTick),
2833 last_change_pcnt: Some(0.0183),
2834 mid_price: Some(55500.0),
2835 impact_bid_price: Some(55490.0),
2836 impact_mid_price: Some(55495.0),
2837 impact_ask_price: Some(55500.0),
2838 fair_method: Some(BitmexFairMethod::ImpactMidPrice),
2839 fair_basis_rate: Some(1.8264),
2840 fair_basis: Some(1000.0),
2841 fair_price: Some(55500.0),
2842 mark_method: Some(BitmexMarkMethod::FairPrice),
2843 indicative_settle_price: Some(55500.0),
2844 settled_price_adjustment_rate: None,
2845 settled_price: None,
2846 instant_pnl: false,
2847 min_tick: None,
2848 capped: None,
2849 opening_timestamp: None,
2850 closing_timestamp: None,
2851 prev_total_volume: None,
2852 }
2853 }
2854
2855 #[rstest]
2856 fn test_parse_spot_instrument() {
2857 let instrument = create_test_spot_instrument();
2858 let ts_init = UnixNanos::default();
2859 let result = parse_spot_instrument(&instrument, ts_init).unwrap();
2860
2861 match result {
2863 InstrumentAny::CurrencyPair(spot) => {
2864 assert_eq!(spot.id.symbol.as_str(), "XBTUSD");
2865 assert_eq!(spot.id.venue.as_str(), "BITMEX");
2866 assert_eq!(spot.raw_symbol.as_str(), "XBTUSD");
2867 assert_eq!(spot.price_precision, 2);
2868 assert_eq!(spot.size_precision, 4);
2869 assert_eq!(spot.price_increment.as_f64(), 0.01);
2870 assert!((spot.size_increment.as_f64() - 0.0001).abs() < 1e-9);
2871 assert!((spot.lot_size.unwrap().as_f64() - 0.1).abs() < 1e-9);
2872 assert_eq!(spot.maker_fee.to_f64().unwrap(), -0.00025);
2873 assert_eq!(spot.taker_fee.to_f64().unwrap(), 0.00075);
2874 }
2875 _ => panic!("Expected CurrencyPair variant"),
2876 }
2877 }
2878
2879 #[rstest]
2880 fn test_parse_perpetual_instrument() {
2881 let instrument = create_test_perpetual_instrument();
2882 let ts_init = UnixNanos::default();
2883 let result = parse_perpetual_instrument(&instrument, ts_init).unwrap();
2884
2885 match result {
2887 InstrumentAny::CryptoPerpetual(perp) => {
2888 assert_eq!(perp.id.symbol.as_str(), "XBTUSD");
2889 assert_eq!(perp.id.venue.as_str(), "BITMEX");
2890 assert_eq!(perp.raw_symbol.as_str(), "XBTUSD");
2891 assert_eq!(perp.price_precision, 1);
2892 assert_eq!(perp.size_precision, 0);
2893 assert_eq!(perp.price_increment.as_f64(), 0.5);
2894 assert_eq!(perp.size_increment.as_f64(), 1.0);
2895 assert_eq!(perp.multiplier, Quantity::from(100_000_000));
2896 assert_eq!(perp.maker_fee.to_f64().unwrap(), -0.00025);
2897 assert_eq!(perp.taker_fee.to_f64().unwrap(), 0.00075);
2898 assert!(perp.is_inverse);
2899 }
2900 _ => panic!("Expected CryptoPerpetual variant"),
2901 }
2902 }
2903
2904 #[rstest]
2905 #[case("USD", "XBt", 1.0, "0.00000001", "0.00001300 XBT")]
2906 #[case("USD", "XBt", 100.0, "0.00000100", "0.00130000 XBT")]
2907 #[case("USD", "XBt", 1_000_000_000.0, "10.00000000", "13000.00000000 XBT")]
2908 #[case("JPY", "USDt", 10_000.0, "0.01000000", "13.00000000 USDT")]
2909 fn test_parse_quanto_perpetual_multiplier_in_settlement_units(
2910 #[case] quote_currency: &str,
2911 #[case] settl_currency: &str,
2912 #[case] multiplier: f64,
2913 #[case] expected_multiplier: &str,
2914 #[case] expected_notional: &str,
2915 ) {
2916 let mut definition = create_test_perpetual_instrument();
2917 definition.symbol = Ustr::from("ETHUSD");
2918 definition.root_symbol = Ustr::from("ETH");
2919 definition.underlying = Ustr::from("ETH");
2920 definition.quote_currency = Ustr::from(quote_currency);
2921 definition.lot_size = Some(1.0);
2922 definition.multiplier = multiplier;
2923 definition.settl_currency = Some(Ustr::from(settl_currency));
2924 definition.underlying_to_position_multiplier = None;
2925 definition.underlying_to_settle_multiplier = None;
2926 definition.quote_to_settle_multiplier = Some(1_510.0);
2927 definition.is_quanto = true;
2928 definition.is_inverse = false;
2929
2930 let result = parse_perpetual_instrument(&definition, UnixNanos::default()).unwrap();
2931 let InstrumentAny::CryptoPerpetual(instrument) = result else {
2932 panic!("Expected CryptoPerpetual variant");
2933 };
2934 let notional =
2935 instrument.calculate_notional_value(Quantity::from(1), Price::from("1300"), None);
2936
2937 assert!(instrument.is_quanto());
2938 assert_eq!(instrument.multiplier, Quantity::from(expected_multiplier));
2939 assert_eq!(notional, Money::from(expected_notional));
2940 }
2941
2942 #[rstest]
2943 fn test_parse_futures_instrument() {
2944 let instrument = create_test_futures_instrument();
2945 let ts_init = UnixNanos::default();
2946 let result = parse_futures_instrument(&instrument, ts_init).unwrap();
2947
2948 match result {
2950 InstrumentAny::CryptoFuture(instrument) => {
2951 assert_eq!(instrument.id.symbol.as_str(), "XBTH25");
2952 assert_eq!(instrument.id.venue.as_str(), "BITMEX");
2953 assert_eq!(instrument.raw_symbol.as_str(), "XBTH25");
2954 assert_eq!(instrument.underlying.code.as_str(), "XBT");
2955 assert_eq!(instrument.price_precision, 1);
2956 assert_eq!(instrument.size_precision, 0);
2957 assert_eq!(instrument.price_increment.as_f64(), 0.5);
2958 assert_eq!(instrument.size_increment.as_f64(), 1.0);
2959 assert_eq!(instrument.multiplier, Quantity::from(100_000_000));
2960 assert_eq!(instrument.maker_fee.to_f64().unwrap(), -0.00025);
2961 assert_eq!(instrument.taker_fee.to_f64().unwrap(), 0.00075);
2962 assert!(instrument.is_inverse);
2963 assert!(instrument.expiration_ns.as_u64() > 0);
2966 }
2967 _ => panic!("Expected CryptoFuture variant"),
2968 }
2969 }
2970
2971 #[rstest]
2972 fn test_parse_quanto_futures_multiplier_in_settlement_units() {
2973 let mut definition = create_test_futures_instrument();
2974 definition.symbol = Ustr::from("ETHUSDU26");
2975 definition.root_symbol = Ustr::from("ETH");
2976 definition.underlying = Ustr::from("ETH");
2977 definition.lot_size = Some(1.0);
2978 definition.multiplier = 100.0;
2979 definition.settl_currency = Some(Ustr::from("XBt"));
2980 definition.underlying_to_position_multiplier = None;
2981 definition.underlying_to_settle_multiplier = None;
2982 definition.quote_to_settle_multiplier = Some(1_510.0);
2983 definition.is_quanto = true;
2984 definition.is_inverse = false;
2985
2986 let result = parse_futures_instrument(&definition, UnixNanos::default()).unwrap();
2987 let InstrumentAny::CryptoFuture(instrument) = result else {
2988 panic!("Expected CryptoFuture variant");
2989 };
2990 let notional =
2991 instrument.calculate_notional_value(Quantity::from(1), Price::from("1300"), None);
2992
2993 assert!(instrument.is_quanto());
2994 assert_eq!(instrument.multiplier, Quantity::from("0.00000100"));
2995 assert_eq!(notional, Money::from("0.00130000 XBT"));
2996 }
2997
2998 #[rstest]
2999 fn test_parse_order_status_report_missing_ord_status_infers_filled() {
3000 let order = BitmexOrder {
3001 account: 123456,
3002 symbol: Some(Ustr::from("XBTUSD")),
3003 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3004 cl_ord_id: Some(Ustr::from("client-filled")),
3005 cl_ord_link_id: None,
3006 side: Some(BitmexSide::Buy),
3007 ord_type: Some(BitmexOrderType::Limit),
3008 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3009 ord_status: None, order_qty: Some(100),
3011 cum_qty: Some(100), price: Some(50000.0),
3013 stop_px: None,
3014 display_qty: None,
3015 peg_offset_value: None,
3016 peg_price_type: None,
3017 currency: Some(Ustr::from("USD")),
3018 settl_currency: Some(Ustr::from("XBt")),
3019 exec_inst: None,
3020 contingency_type: None,
3021 ex_destination: None,
3022 triggered: None,
3023 working_indicator: Some(false),
3024 ord_rej_reason: None,
3025 leaves_qty: Some(0), avg_px: Some(Decimal::from_str("30000.500000000004").unwrap()),
3027 multi_leg_reporting_type: None,
3028 text: None,
3029 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3030 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3031 };
3032
3033 let instrument =
3034 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3035 .unwrap();
3036 let report =
3037 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3038 .unwrap();
3039
3040 assert_eq!(report.order_status, OrderStatus::Filled);
3041 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
3042 assert_eq!(report.filled_qty.as_f64(), 100.0);
3043 assert_eq!(
3044 report.avg_px,
3045 Some(Decimal::from_str("30000.500000000004").unwrap())
3046 );
3047 }
3048
3049 #[rstest]
3050 fn test_parse_order_status_report_missing_ord_status_infers_canceled() {
3051 let order = BitmexOrder {
3052 account: 123456,
3053 symbol: Some(Ustr::from("XBTUSD")),
3054 order_id: Uuid::parse_str("b2c3d4e5-f6a7-8901-bcde-f12345678901").unwrap(),
3055 cl_ord_id: Some(Ustr::from("client-canceled")),
3056 cl_ord_link_id: None,
3057 side: Some(BitmexSide::Sell),
3058 ord_type: Some(BitmexOrderType::Limit),
3059 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3060 ord_status: None, order_qty: Some(200),
3062 cum_qty: Some(0), price: Some(60000.0),
3064 stop_px: None,
3065 display_qty: None,
3066 peg_offset_value: None,
3067 peg_price_type: None,
3068 currency: Some(Ustr::from("USD")),
3069 settl_currency: Some(Ustr::from("XBt")),
3070 exec_inst: None,
3071 contingency_type: None,
3072 ex_destination: None,
3073 triggered: None,
3074 working_indicator: Some(false),
3075 ord_rej_reason: None,
3076 leaves_qty: Some(0), avg_px: None,
3078 multi_leg_reporting_type: None,
3079 text: Some(Ustr::from("Canceled: Already filled")),
3080 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3081 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3082 };
3083
3084 let instrument =
3085 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3086 .unwrap();
3087 let report =
3088 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3089 .unwrap();
3090
3091 assert_eq!(report.order_status, OrderStatus::Canceled);
3092 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
3093 assert_eq!(report.filled_qty.as_f64(), 0.0);
3094 assert_eq!(
3096 report.cancel_reason.as_ref().unwrap(),
3097 "Canceled: Already filled"
3098 );
3099 }
3100
3101 #[rstest]
3102 fn test_parse_order_status_report_missing_ord_status_with_leaves_qty_fails() {
3103 let order = BitmexOrder {
3104 account: 123456,
3105 symbol: Some(Ustr::from("XBTUSD")),
3106 order_id: Uuid::parse_str("c3d4e5f6-a7b8-9012-cdef-123456789012").unwrap(),
3107 cl_ord_id: Some(Ustr::from("client-partial")),
3108 cl_ord_link_id: None,
3109 side: Some(BitmexSide::Buy),
3110 ord_type: Some(BitmexOrderType::Limit),
3111 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3112 ord_status: None, order_qty: Some(100),
3114 cum_qty: Some(50),
3115 price: Some(50000.0),
3116 stop_px: None,
3117 display_qty: None,
3118 peg_offset_value: None,
3119 peg_price_type: None,
3120 currency: Some(Ustr::from("USD")),
3121 settl_currency: Some(Ustr::from("XBt")),
3122 exec_inst: None,
3123 contingency_type: None,
3124 ex_destination: None,
3125 triggered: None,
3126 working_indicator: Some(true),
3127 ord_rej_reason: None,
3128 leaves_qty: Some(50), avg_px: None,
3130 multi_leg_reporting_type: None,
3131 text: None,
3132 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3133 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3134 };
3135
3136 let instrument =
3137 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3138 .unwrap();
3139 let result =
3140 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
3141
3142 assert!(result.is_err());
3143 let err_msg = result.unwrap_err().to_string();
3144 assert!(err_msg.contains("missing ord_status"));
3145 assert!(err_msg.contains("cannot infer"));
3146 }
3147
3148 #[rstest]
3149 fn test_parse_order_status_report_missing_ord_status_no_quantities_fails() {
3150 let order = BitmexOrder {
3151 account: 123456,
3152 symbol: Some(Ustr::from("XBTUSD")),
3153 order_id: Uuid::parse_str("d4e5f6a7-b8c9-0123-def0-123456789013").unwrap(),
3154 cl_ord_id: Some(Ustr::from("client-unknown")),
3155 cl_ord_link_id: None,
3156 side: Some(BitmexSide::Buy),
3157 ord_type: Some(BitmexOrderType::Limit),
3158 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3159 ord_status: None, order_qty: Some(100),
3161 cum_qty: None, price: Some(50000.0),
3163 stop_px: None,
3164 display_qty: None,
3165 peg_offset_value: None,
3166 peg_price_type: None,
3167 currency: Some(Ustr::from("USD")),
3168 settl_currency: Some(Ustr::from("XBt")),
3169 exec_inst: None,
3170 contingency_type: None,
3171 ex_destination: None,
3172 triggered: None,
3173 working_indicator: Some(true),
3174 ord_rej_reason: None,
3175 leaves_qty: None, avg_px: None,
3177 multi_leg_reporting_type: None,
3178 text: None,
3179 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3180 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3181 };
3182
3183 let instrument =
3184 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3185 .unwrap();
3186 let result =
3187 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1));
3188
3189 assert!(result.is_err());
3190 let err_msg = result.unwrap_err().to_string();
3191 assert!(err_msg.contains("missing ord_status"));
3192 assert!(err_msg.contains("cannot infer"));
3193 }
3194
3195 #[rstest]
3196 fn test_parse_order_status_report_infers_market_order_type() {
3197 let order = BitmexOrder {
3199 account: 123456,
3200 symbol: Some(Ustr::from("XBTUSD")),
3201 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3202 cl_ord_id: Some(Ustr::from("client-123")),
3203 cl_ord_link_id: None,
3204 side: Some(BitmexSide::Buy),
3205 ord_type: None,
3206 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3207 ord_status: Some(BitmexOrderStatus::Filled),
3208 order_qty: Some(100),
3209 cum_qty: Some(100),
3210 price: None,
3211 stop_px: None,
3212 display_qty: None,
3213 peg_offset_value: None,
3214 peg_price_type: None,
3215 currency: Some(Ustr::from("USD")),
3216 settl_currency: Some(Ustr::from("XBt")),
3217 exec_inst: None,
3218 contingency_type: None,
3219 ex_destination: None,
3220 triggered: None,
3221 working_indicator: None,
3222 ord_rej_reason: None,
3223 leaves_qty: Some(0),
3224 avg_px: Some(Decimal::from_str("50000.0").unwrap()),
3225 multi_leg_reporting_type: None,
3226 text: None,
3227 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3228 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3229 };
3230
3231 let instrument =
3232 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3233 .unwrap();
3234 let report =
3235 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3236 .unwrap();
3237
3238 assert_eq!(report.order_type, OrderType::Market);
3239 }
3240
3241 #[rstest]
3242 fn test_parse_order_status_report_infers_limit_order_type() {
3243 let order = BitmexOrder {
3245 account: 123456,
3246 symbol: Some(Ustr::from("XBTUSD")),
3247 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3248 cl_ord_id: Some(Ustr::from("client-123")),
3249 cl_ord_link_id: None,
3250 side: Some(BitmexSide::Buy),
3251 ord_type: None,
3252 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3253 ord_status: Some(BitmexOrderStatus::New),
3254 order_qty: Some(100),
3255 cum_qty: Some(0),
3256 price: Some(50000.0),
3257 stop_px: None,
3258 display_qty: None,
3259 peg_offset_value: None,
3260 peg_price_type: None,
3261 currency: Some(Ustr::from("USD")),
3262 settl_currency: Some(Ustr::from("XBt")),
3263 exec_inst: None,
3264 contingency_type: None,
3265 ex_destination: None,
3266 triggered: None,
3267 working_indicator: Some(true),
3268 ord_rej_reason: None,
3269 leaves_qty: Some(100),
3270 avg_px: None,
3271 multi_leg_reporting_type: None,
3272 text: None,
3273 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3274 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3275 };
3276
3277 let instrument =
3278 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3279 .unwrap();
3280 let report =
3281 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3282 .unwrap();
3283
3284 assert_eq!(report.order_type, OrderType::Limit);
3285 }
3286
3287 #[rstest]
3288 fn test_parse_order_status_report_infers_stop_market_order_type() {
3289 let order = BitmexOrder {
3291 account: 123456,
3292 symbol: Some(Ustr::from("XBTUSD")),
3293 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3294 cl_ord_id: Some(Ustr::from("client-123")),
3295 cl_ord_link_id: None,
3296 side: Some(BitmexSide::Sell),
3297 ord_type: None,
3298 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3299 ord_status: Some(BitmexOrderStatus::New),
3300 order_qty: Some(100),
3301 cum_qty: Some(0),
3302 price: None,
3303 stop_px: Some(45000.0),
3304 display_qty: None,
3305 peg_offset_value: None,
3306 peg_price_type: None,
3307 currency: Some(Ustr::from("USD")),
3308 settl_currency: Some(Ustr::from("XBt")),
3309 exec_inst: None,
3310 contingency_type: None,
3311 ex_destination: None,
3312 triggered: None,
3313 working_indicator: Some(false),
3314 ord_rej_reason: None,
3315 leaves_qty: Some(100),
3316 avg_px: None,
3317 multi_leg_reporting_type: None,
3318 text: None,
3319 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3320 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3321 };
3322
3323 let instrument =
3324 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3325 .unwrap();
3326 let report =
3327 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3328 .unwrap();
3329
3330 assert_eq!(report.order_type, OrderType::StopMarket);
3331 }
3332
3333 #[rstest]
3334 fn test_parse_order_status_report_infers_stop_limit_order_type() {
3335 let order = BitmexOrder {
3337 account: 123456,
3338 symbol: Some(Ustr::from("XBTUSD")),
3339 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3340 cl_ord_id: Some(Ustr::from("client-123")),
3341 cl_ord_link_id: None,
3342 side: Some(BitmexSide::Sell),
3343 ord_type: None,
3344 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3345 ord_status: Some(BitmexOrderStatus::New),
3346 order_qty: Some(100),
3347 cum_qty: Some(0),
3348 price: Some(44000.0),
3349 stop_px: Some(45000.0),
3350 display_qty: None,
3351 peg_offset_value: None,
3352 peg_price_type: None,
3353 currency: Some(Ustr::from("USD")),
3354 settl_currency: Some(Ustr::from("XBt")),
3355 exec_inst: None,
3356 contingency_type: None,
3357 ex_destination: None,
3358 triggered: None,
3359 working_indicator: Some(false),
3360 ord_rej_reason: None,
3361 leaves_qty: Some(100),
3362 avg_px: None,
3363 multi_leg_reporting_type: None,
3364 text: None,
3365 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3366 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3367 };
3368
3369 let instrument =
3370 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3371 .unwrap();
3372 let report =
3373 parse_order_status_report(&order, &instrument, &DashMap::default(), UnixNanos::from(1))
3374 .unwrap();
3375
3376 assert_eq!(report.order_type, OrderType::StopLimit);
3377 }
3378
3379 #[rstest]
3380 fn test_parse_order_status_report_uses_cached_order_type() {
3381 let order = BitmexOrder {
3383 account: 123456,
3384 symbol: Some(Ustr::from("XBTUSD")),
3385 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
3386 cl_ord_id: Some(Ustr::from("client-123")),
3387 cl_ord_link_id: None,
3388 side: Some(BitmexSide::Buy),
3389 ord_type: None,
3390 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
3391 ord_status: Some(BitmexOrderStatus::Canceled),
3392 order_qty: None,
3393 cum_qty: Some(0),
3394 price: None,
3395 stop_px: None,
3396 display_qty: None,
3397 peg_offset_value: None,
3398 peg_price_type: None,
3399 currency: Some(Ustr::from("USD")),
3400 settl_currency: Some(Ustr::from("XBt")),
3401 exec_inst: None,
3402 contingency_type: None,
3403 ex_destination: None,
3404 triggered: None,
3405 working_indicator: None,
3406 ord_rej_reason: None,
3407 leaves_qty: Some(0),
3408 avg_px: None,
3409 multi_leg_reporting_type: None,
3410 text: None,
3411 transact_time: Some("2024-01-01T00:00:00Z".parse::<Timestamp>().unwrap()),
3412 timestamp: Some("2024-01-01T00:00:01Z".parse::<Timestamp>().unwrap()),
3413 };
3414
3415 let instrument =
3416 parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
3417 .unwrap();
3418
3419 let cache: DashMap<ClientOrderId, OrderType> = DashMap::new();
3421 cache.insert(ClientOrderId::new("client-123"), OrderType::StopLimit);
3422
3423 let report =
3424 parse_order_status_report(&order, &instrument, &cache, UnixNanos::from(1)).unwrap();
3425
3426 assert_eq!(report.order_type, OrderType::StopLimit);
3427 }
3428}