1use std::str::FromStr;
17
18use nautilus_core::{UnixNanos, time::get_atomic_clock_realtime, uuid::UUID4};
19use nautilus_model::{
20 currencies::CURRENCY_MAP,
21 data::TradeTick,
22 enums::{CurrencyType, OrderSide, OrderStatus, OrderType, TriggerType},
23 identifiers::{
24 AccountId, ClientOrderId, InstrumentId, OrderListId, Symbol, TradeId, VenueOrderId,
25 },
26 instruments::{CryptoFuture, CryptoPerpetual, CurrencyPair, InstrumentAny},
27 reports::{FillReport, OrderStatusReport, PositionStatusReport},
28 types::{Currency, Money, Price, Quantity},
29};
30use rust_decimal::Decimal;
31use ustr::Ustr;
32use uuid::Uuid;
33
34use super::models::{BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTrade};
35use crate::common::{
36 enums::{BitmexExecInstruction, BitmexExecType, BitmexInstrumentType},
37 parse::{
38 map_bitmex_currency, parse_aggressor_side, parse_instrument_id, parse_liquidity_side,
39 parse_optional_datetime_to_unix_nanos, parse_position_side,
40 },
41};
42
43#[must_use]
44pub fn parse_instrument_any(
45 instrument: &BitmexInstrument,
46 ts_init: UnixNanos,
47) -> Option<InstrumentAny> {
48 match instrument.instrument_type {
49 BitmexInstrumentType::Spot => parse_spot_instrument(instrument, ts_init)
50 .map_err(|e| {
51 tracing::warn!("Failed to parse spot instrument {}: {e}", instrument.symbol);
52 e
53 })
54 .ok(),
55 BitmexInstrumentType::PerpetualContract | BitmexInstrumentType::PerpetualContractFx => {
56 parse_perpetual_instrument(instrument, ts_init)
58 .map_err(|e| {
59 tracing::warn!(
60 "Failed to parse perpetual instrument {}: {e}",
61 instrument.symbol,
62 );
63 e
64 })
65 .ok()
66 }
67 BitmexInstrumentType::Futures => parse_futures_instrument(instrument, ts_init)
68 .map_err(|e| {
69 tracing::warn!(
70 "Failed to parse futures instrument {}: {e}",
71 instrument.symbol,
72 );
73 e
74 })
75 .ok(),
76 BitmexInstrumentType::BasketIndex
77 | BitmexInstrumentType::CryptoIndex
78 | BitmexInstrumentType::FxIndex
79 | BitmexInstrumentType::LendingIndex
80 | BitmexInstrumentType::VolatilityIndex => {
81 parse_index_instrument(instrument, ts_init)
84 .map_err(|e| {
85 tracing::warn!(
86 "Failed to parse index instrument {}: {}",
87 instrument.symbol,
88 e
89 );
90 e
91 })
92 .ok()
93 }
94 _ => {
95 tracing::warn!(
96 "Unsupported instrument type {:?} for symbol {}",
97 instrument.instrument_type,
98 instrument.symbol
99 );
100 None
101 }
102 }
103}
104
105pub fn parse_index_instrument(
114 definition: &BitmexInstrument,
115 ts_init: UnixNanos,
116) -> anyhow::Result<InstrumentAny> {
117 let instrument_id = parse_instrument_id(definition.symbol);
118 let raw_symbol = Symbol::new(definition.symbol);
119
120 let base_currency = Currency::USD();
121 let quote_currency = Currency::USD();
122 let settlement_currency = Currency::USD();
123
124 let price_increment = Price::from(definition.tick_size.to_string());
125 let size_increment = Quantity::from(1); Ok(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
128 instrument_id,
129 raw_symbol,
130 base_currency,
131 quote_currency,
132 settlement_currency,
133 false, price_increment.precision,
135 size_increment.precision,
136 price_increment,
137 size_increment,
138 None, None, None, None, None, None, None, None, None, None, None, None, ts_init,
151 ts_init,
152 )))
153}
154
155pub fn parse_spot_instrument(
161 definition: &BitmexInstrument,
162 ts_init: UnixNanos,
163) -> anyhow::Result<InstrumentAny> {
164 let instrument_id = parse_instrument_id(definition.symbol);
165 let raw_symbol = Symbol::new(definition.symbol);
166 let base_currency = get_currency(definition.underlying.to_uppercase());
167 let quote_currency = get_currency(definition.quote_currency.to_uppercase());
168
169 let price_increment = Price::from(definition.tick_size.to_string());
170
171 let actual_lot_size = if let Some(multiplier) = definition.underlying_to_position_multiplier {
174 definition.lot_size.unwrap_or(1.0) / multiplier
175 } else {
176 definition.lot_size.unwrap_or(1.0)
177 };
178
179 let size_increment = Quantity::from(actual_lot_size.to_string());
180
181 let taker_fee = definition
182 .taker_fee
183 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
184 .unwrap_or(Decimal::ZERO);
185 let maker_fee = definition
186 .maker_fee
187 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
188 .unwrap_or(Decimal::ZERO);
189
190 let margin_init = definition
191 .init_margin
192 .as_ref()
193 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
194 .unwrap_or(Decimal::ZERO);
195 let margin_maint = definition
196 .maint_margin
197 .as_ref()
198 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
199 .unwrap_or(Decimal::ZERO);
200
201 let lot_size = definition
202 .lot_size
203 .map(|size| Quantity::new_checked(size, 0))
204 .transpose()?;
205 let max_quantity = definition
206 .max_order_qty
207 .map(|qty| Quantity::new_checked(qty, 0))
208 .transpose()?;
209 let min_quantity = definition
210 .lot_size
211 .map(|size| Quantity::new_checked(size, 0))
212 .transpose()?;
213 let max_notional: Option<Money> = None;
214 let min_notional: Option<Money> = None;
215 let max_price = definition
216 .max_price
217 .map(|price| Price::from(price.to_string()));
218 let min_price = None;
219 let ts_event = UnixNanos::from(definition.timestamp);
220
221 let instrument = CurrencyPair::new(
222 instrument_id,
223 raw_symbol,
224 base_currency,
225 quote_currency,
226 price_increment.precision,
227 size_increment.precision,
228 price_increment,
229 size_increment,
230 None, lot_size,
232 max_quantity,
233 min_quantity,
234 max_notional,
235 min_notional,
236 max_price,
237 min_price,
238 Some(margin_init),
239 Some(margin_maint),
240 Some(maker_fee),
241 Some(taker_fee),
242 ts_event,
243 ts_init,
244 );
245
246 Ok(InstrumentAny::CurrencyPair(instrument))
247}
248
249pub fn parse_perpetual_instrument(
255 definition: &BitmexInstrument,
256 ts_init: UnixNanos,
257) -> anyhow::Result<InstrumentAny> {
258 let instrument_id = parse_instrument_id(definition.symbol);
259 let raw_symbol = Symbol::new(definition.symbol);
260 let base_currency = get_currency(definition.underlying.to_uppercase());
261 let quote_currency = get_currency(definition.quote_currency.to_uppercase());
262 let settlement_currency = get_currency(definition.settl_currency.as_ref().map_or_else(
263 || definition.quote_currency.to_uppercase(),
264 |s| s.to_uppercase(),
265 ));
266 let is_inverse = definition.is_inverse;
267
268 let price_increment = Price::from(definition.tick_size.to_string());
269
270 let actual_lot_size = if let Some(multiplier) = definition.underlying_to_position_multiplier {
273 definition.lot_size.unwrap_or(1.0) / multiplier
274 } else {
275 definition.lot_size.unwrap_or(1.0)
276 };
277
278 let size_increment = Quantity::from(actual_lot_size.to_string());
279
280 let taker_fee = definition
281 .taker_fee
282 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
283 .unwrap_or(Decimal::ZERO);
284 let maker_fee = definition
285 .maker_fee
286 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
287 .unwrap_or(Decimal::ZERO);
288
289 let margin_init = definition
290 .init_margin
291 .as_ref()
292 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
293 .unwrap_or(Decimal::ZERO);
294 let margin_maint = definition
295 .maint_margin
296 .as_ref()
297 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
298 .unwrap_or(Decimal::ZERO);
299
300 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
302 let lot_size = definition
303 .lot_size
304 .map(|size| Quantity::new_checked(size, 0))
305 .transpose()?;
306 let max_quantity = definition
307 .max_order_qty
308 .map(|qty| Quantity::new_checked(qty, 0))
309 .transpose()?;
310 let min_quantity = definition
311 .lot_size
312 .map(|size| Quantity::new_checked(size, 0))
313 .transpose()?;
314 let max_notional: Option<Money> = None;
315 let min_notional: Option<Money> = None;
316 let max_price = definition
317 .max_price
318 .map(|price| Price::from(price.to_string()));
319 let min_price = None;
320 let ts_event = UnixNanos::from(definition.timestamp);
321
322 let instrument = CryptoPerpetual::new(
323 instrument_id,
324 raw_symbol,
325 base_currency,
326 quote_currency,
327 settlement_currency,
328 is_inverse,
329 price_increment.precision,
330 size_increment.precision,
331 price_increment,
332 size_increment,
333 multiplier,
334 lot_size,
335 max_quantity,
336 min_quantity,
337 max_notional,
338 min_notional,
339 max_price,
340 min_price,
341 Some(margin_init),
342 Some(margin_maint),
343 Some(maker_fee),
344 Some(taker_fee),
345 ts_event,
346 ts_init,
347 );
348
349 Ok(InstrumentAny::CryptoPerpetual(instrument))
350}
351
352pub fn parse_futures_instrument(
358 definition: &BitmexInstrument,
359 ts_init: UnixNanos,
360) -> anyhow::Result<InstrumentAny> {
361 let instrument_id = parse_instrument_id(definition.symbol);
362 let raw_symbol = Symbol::new(definition.symbol);
363 let underlying = get_currency(definition.underlying.to_uppercase());
364 let quote_currency = get_currency(definition.quote_currency.to_uppercase());
365 let settlement_currency = get_currency(definition.settl_currency.as_ref().map_or_else(
366 || definition.quote_currency.to_uppercase(),
367 |s| s.to_uppercase(),
368 ));
369 let is_inverse = definition.is_inverse;
370
371 let activation_ns = UnixNanos::from(definition.listing);
372 let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
373 let price_increment = Price::from(definition.tick_size.to_string());
374
375 let actual_lot_size = if let Some(multiplier) = definition.underlying_to_position_multiplier {
378 definition.lot_size.unwrap_or(1.0) / multiplier
379 } else {
380 definition.lot_size.unwrap_or(1.0)
381 };
382
383 let size_increment = Quantity::from(actual_lot_size.to_string());
384
385 let taker_fee = definition
386 .taker_fee
387 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
388 .unwrap_or(Decimal::ZERO);
389 let maker_fee = definition
390 .maker_fee
391 .and_then(|fee| Decimal::from_str(&fee.to_string()).ok())
392 .unwrap_or(Decimal::ZERO);
393
394 let margin_init = definition
395 .init_margin
396 .as_ref()
397 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
398 .unwrap_or(Decimal::ZERO);
399 let margin_maint = definition
400 .maint_margin
401 .as_ref()
402 .and_then(|margin| Decimal::from_str(&margin.to_string()).ok())
403 .unwrap_or(Decimal::ZERO);
404
405 let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
407
408 let lot_size = definition
409 .lot_size
410 .map(|size| Quantity::new_checked(size, 0))
411 .transpose()?;
412 let max_quantity = definition
413 .max_order_qty
414 .map(|qty| Quantity::new_checked(qty, 0))
415 .transpose()?;
416 let min_quantity = definition
417 .lot_size
418 .map(|size| Quantity::new_checked(size, 0))
419 .transpose()?;
420 let max_notional: Option<Money> = None;
421 let min_notional: Option<Money> = None;
422 let max_price = definition
423 .max_price
424 .map(|price| Price::from(price.to_string()));
425 let min_price = None;
426 let ts_event = UnixNanos::from(definition.timestamp);
427
428 let instrument = CryptoFuture::new(
429 instrument_id,
430 raw_symbol,
431 underlying,
432 quote_currency,
433 settlement_currency,
434 is_inverse,
435 activation_ns,
436 expiration_ns,
437 price_increment.precision,
438 size_increment.precision,
439 price_increment,
440 size_increment,
441 multiplier,
442 lot_size,
443 max_quantity,
444 min_quantity,
445 max_notional,
446 min_notional,
447 max_price,
448 min_price,
449 Some(margin_init),
450 Some(margin_maint),
451 Some(maker_fee),
452 Some(taker_fee),
453 ts_event,
454 ts_init,
455 );
456
457 Ok(InstrumentAny::CryptoFuture(instrument))
458}
459
460pub fn parse_trade(
467 trade: BitmexTrade,
468 price_precision: u8,
469 ts_init: UnixNanos,
470) -> anyhow::Result<TradeTick> {
471 let instrument_id = parse_instrument_id(trade.symbol);
472 let price = Price::new(trade.price, price_precision);
473 let size = Quantity::from(trade.size);
474 let aggressor_side = parse_aggressor_side(&trade.side);
475 let trade_id = TradeId::new(
476 trade
477 .trd_match_id
478 .map_or_else(|| Uuid::new_v4().to_string(), |uuid| uuid.to_string()),
479 );
480 let ts_event = UnixNanos::from(trade.timestamp);
481
482 Ok(TradeTick::new(
483 instrument_id,
484 price,
485 size,
486 aggressor_side,
487 trade_id,
488 ts_event,
489 ts_init,
490 ))
491}
492
493pub fn parse_order_status_report(
506 order: &BitmexOrder,
507 instrument_id: InstrumentId,
508 price_precision: u8,
509 ts_init: UnixNanos,
510) -> anyhow::Result<OrderStatusReport> {
511 let account_id = AccountId::new(format!("BITMEX-{}", order.account));
512 let venue_order_id = VenueOrderId::new(order.order_id.to_string());
513 let order_side: OrderSide = order
514 .side
515 .map_or(OrderSide::NoOrderSide, |side| side.into());
516 let order_type: OrderType = (*order
517 .ord_type
518 .as_ref()
519 .ok_or_else(|| anyhow::anyhow!("Order missing ord_type"))?)
520 .into();
521 let time_in_force: nautilus_model::enums::TimeInForce = (*order
522 .time_in_force
523 .as_ref()
524 .ok_or_else(|| anyhow::anyhow!("Order missing time_in_force"))?)
525 .try_into()
526 .map_err(|e| anyhow::anyhow!("{e}"))?;
527 let order_status: OrderStatus = (*order
528 .ord_status
529 .as_ref()
530 .ok_or_else(|| anyhow::anyhow!("Order missing ord_status"))?)
531 .into();
532 let quantity = Quantity::from(
533 order
534 .order_qty
535 .ok_or_else(|| anyhow::anyhow!("Order missing order_qty"))?,
536 );
537 let filled_qty = Quantity::from(order.cum_qty.unwrap_or(0));
538 let report_id = UUID4::new();
539 let ts_accepted = order.transact_time.map_or_else(
540 || get_atomic_clock_realtime().get_time_ns(),
541 UnixNanos::from,
542 );
543 let ts_last = order.timestamp.map_or_else(
544 || get_atomic_clock_realtime().get_time_ns(),
545 UnixNanos::from,
546 );
547
548 let mut report = OrderStatusReport::new(
549 account_id,
550 instrument_id,
551 None, venue_order_id,
553 order_side,
554 order_type,
555 time_in_force,
556 order_status,
557 quantity,
558 filled_qty,
559 ts_accepted,
560 ts_last,
561 ts_init,
562 Some(report_id),
563 );
564
565 if let Some(cl_ord_id) = order.cl_ord_id {
566 report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
567 }
568
569 if let Some(cl_ord_link_id) = order.cl_ord_link_id {
570 report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
571 }
572
573 if let Some(price) = order.price {
574 report = report.with_price(Price::new(price, price_precision));
575 }
576
577 if let Some(avg_px) = order.avg_px {
578 report = report.with_avg_px(avg_px);
579 }
580
581 if let Some(trigger_price) = order.stop_px {
582 report = report
583 .with_trigger_price(Price::new(trigger_price, price_precision))
584 .with_trigger_type(TriggerType::Default);
585 }
586
587 if let Some(exec_instructions) = &order.exec_inst {
588 for inst in exec_instructions {
589 match inst {
590 BitmexExecInstruction::ParticipateDoNotInitiate => {
591 report = report.with_post_only(true);
592 }
593 BitmexExecInstruction::ReduceOnly => report = report.with_reduce_only(true),
594 BitmexExecInstruction::LastPrice
595 | BitmexExecInstruction::Close
596 | BitmexExecInstruction::MarkPrice
597 | BitmexExecInstruction::IndexPrice
598 | BitmexExecInstruction::AllOrNone
599 | BitmexExecInstruction::Fixed
600 | BitmexExecInstruction::Unknown => {}
601 }
602 }
603 }
604
605 if let Some(contingency_type) = order.contingency_type {
606 report = report.with_contingency_type(contingency_type.into());
607 }
608
609 Ok(report)
614}
615
616pub fn parse_fill_report(
628 exec: BitmexExecution,
629 price_precision: u8,
630 ts_init: UnixNanos,
631) -> anyhow::Result<FillReport> {
632 if !matches!(exec.exec_type, BitmexExecType::Trade) {
635 return Err(anyhow::anyhow!(
636 "Skipping non-trade execution: {:?}",
637 exec.exec_type
638 ));
639 }
640
641 let order_id = exec.order_id.ok_or_else(|| {
643 anyhow::anyhow!("Skipping execution without order_id: {:?}", exec.exec_type)
644 })?;
645
646 let account_id = AccountId::new(format!("BITMEX-{}", exec.account));
647 let symbol = exec
648 .symbol
649 .ok_or_else(|| anyhow::anyhow!("Execution missing symbol"))?;
650 let instrument_id = parse_instrument_id(symbol);
651 let venue_order_id = VenueOrderId::new(order_id.to_string());
652 let trade_id = TradeId::new(
654 exec.trd_match_id
655 .or(Some(exec.exec_id))
656 .ok_or_else(|| anyhow::anyhow!("Fill missing both trd_match_id and exec_id"))?
657 .to_string(),
658 );
659 let Some(side) = exec.side else {
661 return Err(anyhow::anyhow!(
662 "Skipping execution without side: {:?}",
663 exec.exec_type
664 ));
665 };
666 let order_side: OrderSide = side.into();
667 let last_qty = Quantity::from(exec.last_qty);
668 let last_px = Price::new(exec.last_px, price_precision);
669
670 let settlement_currency_str = exec.settl_currency.unwrap_or(Ustr::from("XBT")).as_str();
672 let mapped_currency = map_bitmex_currency(settlement_currency_str);
673 let commission = Money::new(
674 exec.commission.unwrap_or(0.0),
675 Currency::from(mapped_currency.as_str()),
676 );
677 let liquidity_side = parse_liquidity_side(&exec.last_liquidity_ind);
678 let client_order_id = exec.cl_ord_id.map(ClientOrderId::new);
679 let venue_position_id = None; let ts_event = exec.transact_time.map_or_else(
681 || get_atomic_clock_realtime().get_time_ns(),
682 UnixNanos::from,
683 );
684
685 Ok(FillReport::new(
686 account_id,
687 instrument_id,
688 venue_order_id,
689 trade_id,
690 order_side,
691 last_qty,
692 last_px,
693 commission,
694 liquidity_side,
695 client_order_id,
696 venue_position_id,
697 ts_event,
698 ts_init,
699 None,
700 ))
701}
702
703pub fn parse_position_report(
710 position: BitmexPosition,
711 ts_init: UnixNanos,
712) -> anyhow::Result<PositionStatusReport> {
713 let account_id = AccountId::new(format!("BITMEX-{}", position.account));
714 let instrument_id = parse_instrument_id(position.symbol);
715 let position_side = parse_position_side(position.current_qty).as_specified();
716 let quantity = Quantity::from(position.current_qty.map_or(0_i64, i64::abs));
717 let venue_position_id = None; let ts_last = parse_optional_datetime_to_unix_nanos(&position.timestamp, "timestamp");
719
720 Ok(PositionStatusReport::new(
721 account_id,
722 instrument_id,
723 position_side,
724 quantity,
725 venue_position_id,
726 ts_last,
727 ts_init,
728 None,
729 ))
730}
731
732fn get_currency(code: String) -> Currency {
734 CURRENCY_MAP
735 .lock()
736 .unwrap()
737 .get(&code)
738 .copied()
739 .unwrap_or(Currency::new(&code, 8, 0, &code, CurrencyType::Crypto))
740}
741
742#[cfg(test)]
747mod tests {
748 use chrono::{DateTime, Utc};
749 use nautilus_model::enums::{LiquiditySide, PositionSide};
750 use rstest::rstest;
751 use rust_decimal::prelude::ToPrimitive;
752 use uuid::Uuid;
753
754 use super::*;
755 use crate::{
756 common::{
757 enums::{
758 BitmexContingencyType, BitmexFairMethod, BitmexInstrumentState,
759 BitmexInstrumentType, BitmexLiquidityIndicator, BitmexMarkMethod,
760 BitmexOrderStatus, BitmexOrderType, BitmexSide, BitmexTickDirection,
761 BitmexTimeInForce,
762 },
763 testing::load_test_json,
764 },
765 http::models::{
766 BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTradeBin,
767 BitmexWallet,
768 },
769 };
770
771 #[rstest]
772 fn test_perp_instrument_deserialization() {
773 let json_data = load_test_json("http_get_instrument_xbtusd.json");
774 let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
775
776 assert_eq!(instrument.symbol, "XBTUSD");
777 assert_eq!(instrument.root_symbol, "XBT");
778 assert_eq!(instrument.state, BitmexInstrumentState::Open);
779 assert!(instrument.is_inverse);
780 assert_eq!(instrument.maker_fee, Some(0.0005));
781 assert_eq!(
782 instrument.timestamp.to_rfc3339(),
783 "2024-11-24T23:33:19.034+00:00"
784 );
785 }
786
787 #[rstest]
788 fn test_parse_orders() {
789 let json_data = load_test_json("http_get_orders.json");
790 let orders: Vec<BitmexOrder> = serde_json::from_str(&json_data).unwrap();
791
792 assert_eq!(orders.len(), 2);
793
794 let order1 = &orders[0];
796 assert_eq!(order1.symbol, Some(Ustr::from("XBTUSD")));
797 assert_eq!(order1.side, Some(BitmexSide::Buy));
798 assert_eq!(order1.order_qty, Some(100));
799 assert_eq!(order1.price, Some(98000.0));
800 assert_eq!(order1.ord_status, Some(BitmexOrderStatus::New));
801 assert_eq!(order1.leaves_qty, Some(100));
802 assert_eq!(order1.cum_qty, Some(0));
803
804 let order2 = &orders[1];
806 assert_eq!(order2.symbol, Some(Ustr::from("XBTUSD")));
807 assert_eq!(order2.side, Some(BitmexSide::Sell));
808 assert_eq!(order2.order_qty, Some(200));
809 assert_eq!(order2.ord_status, Some(BitmexOrderStatus::Filled));
810 assert_eq!(order2.leaves_qty, Some(0));
811 assert_eq!(order2.cum_qty, Some(200));
812 assert_eq!(order2.avg_px, Some(98950.5));
813 }
814
815 #[rstest]
816 fn test_parse_executions() {
817 let json_data = load_test_json("http_get_executions.json");
818 let executions: Vec<BitmexExecution> = serde_json::from_str(&json_data).unwrap();
819
820 assert_eq!(executions.len(), 2);
821
822 let exec1 = &executions[0];
824 assert_eq!(exec1.symbol, Some(Ustr::from("XBTUSD")));
825 assert_eq!(exec1.side, Some(BitmexSide::Sell));
826 assert_eq!(exec1.last_qty, 100);
827 assert_eq!(exec1.last_px, 98950.0);
828 assert_eq!(
829 exec1.last_liquidity_ind,
830 Some(BitmexLiquidityIndicator::Maker)
831 );
832 assert_eq!(exec1.commission, Some(0.00075));
833
834 let exec2 = &executions[1];
836 assert_eq!(
837 exec2.last_liquidity_ind,
838 Some(BitmexLiquidityIndicator::Taker)
839 );
840 assert_eq!(exec2.last_px, 98951.0);
841 }
842
843 #[rstest]
844 fn test_parse_positions() {
845 let json_data = load_test_json("http_get_positions.json");
846 let positions: Vec<BitmexPosition> = serde_json::from_str(&json_data).unwrap();
847
848 assert_eq!(positions.len(), 1);
849
850 let position = &positions[0];
851 assert_eq!(position.account, 1234567);
852 assert_eq!(position.symbol, "XBTUSD");
853 assert_eq!(position.current_qty, Some(100));
854 assert_eq!(position.avg_entry_price, Some(98390.88));
855 assert_eq!(position.unrealised_pnl, Some(1350));
856 assert_eq!(position.realised_pnl, Some(-227));
857 assert_eq!(position.is_open, Some(true));
858 }
859
860 #[rstest]
861 fn test_parse_trades() {
862 let json_data = load_test_json("http_get_trades.json");
863 let trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
864
865 assert_eq!(trades.len(), 3);
866
867 let trade1 = &trades[0];
869 assert_eq!(trade1.symbol, "XBTUSD");
870 assert_eq!(trade1.side, Some(BitmexSide::Buy));
871 assert_eq!(trade1.size, 100);
872 assert_eq!(trade1.price, 98950.0);
873
874 let trade3 = &trades[2];
876 assert_eq!(trade3.side, Some(BitmexSide::Sell));
877 assert_eq!(trade3.size, 50);
878 assert_eq!(trade3.price, 98949.5);
879 }
880
881 #[rstest]
882 fn test_parse_wallet() {
883 let json_data = load_test_json("http_get_wallet.json");
884 let wallets: Vec<BitmexWallet> = serde_json::from_str(&json_data).unwrap();
885
886 assert_eq!(wallets.len(), 1);
887
888 let wallet = &wallets[0];
889 assert_eq!(wallet.account, 1234567);
890 assert_eq!(wallet.currency, "XBt");
891 assert_eq!(wallet.amount, Some(1000123456));
892 assert_eq!(wallet.delta_amount, Some(123456));
893 }
894
895 #[rstest]
896 fn test_parse_trade_bins() {
897 let json_data = load_test_json("http_get_trade_bins.json");
898 let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
899
900 assert_eq!(bins.len(), 3);
901
902 let bin1 = &bins[0];
904 assert_eq!(bin1.symbol, "XBTUSD");
905 assert_eq!(bin1.open, Some(98900.0));
906 assert_eq!(bin1.high, Some(98980.5));
907 assert_eq!(bin1.low, Some(98890.0));
908 assert_eq!(bin1.close, Some(98950.0));
909 assert_eq!(bin1.volume, Some(150000));
910 assert_eq!(bin1.trades, Some(45));
911
912 let bin3 = &bins[2];
914 assert_eq!(bin3.close, Some(98970.0));
915 assert_eq!(bin3.volume, Some(78000));
916 }
917
918 #[rstest]
919 fn test_parse_order_status_report() {
920 let symbol = Ustr::from("XBTUSD");
921
922 let order = BitmexOrder {
923 account: 123456,
924 symbol: Some(Ustr::from("XBTUSD")),
925 order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
926 cl_ord_id: Some(Ustr::from("client-123")),
927 cl_ord_link_id: None,
928 side: Some(BitmexSide::Buy),
929 ord_type: Some(BitmexOrderType::Limit),
930 time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
931 ord_status: Some(BitmexOrderStatus::New),
932 order_qty: Some(100),
933 cum_qty: Some(50),
934 price: Some(50000.0),
935 stop_px: Some(49000.0),
936 display_qty: None,
937 peg_offset_value: None,
938 peg_price_type: None,
939 currency: Some(Ustr::from("USD")),
940 settl_currency: Some(Ustr::from("XBt")),
941 exec_inst: Some(vec![
942 BitmexExecInstruction::ParticipateDoNotInitiate,
943 BitmexExecInstruction::ReduceOnly,
944 ]),
945 contingency_type: Some(BitmexContingencyType::OneCancelsTheOther),
946 ex_destination: None,
947 triggered: None,
948 working_indicator: Some(true),
949 ord_rej_reason: None,
950 leaves_qty: Some(50),
951 avg_px: None,
952 multi_leg_reporting_type: None,
953 text: None,
954 transact_time: Some(
955 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
956 .unwrap()
957 .with_timezone(&Utc),
958 ),
959 timestamp: Some(
960 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
961 .unwrap()
962 .with_timezone(&Utc),
963 ),
964 };
965
966 let instrument_id = parse_instrument_id(symbol);
967 let report =
968 parse_order_status_report(&order, instrument_id, 2, UnixNanos::from(1)).unwrap();
969
970 assert_eq!(report.account_id.to_string(), "BITMEX-123456");
971 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
972 assert_eq!(
973 report.venue_order_id.as_str(),
974 "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
975 );
976 assert_eq!(report.client_order_id.unwrap().as_str(), "client-123");
977 assert_eq!(report.quantity.as_f64(), 100.0);
978 assert_eq!(report.filled_qty.as_f64(), 50.0);
979 assert_eq!(report.price.unwrap().as_f64(), 50000.0);
980 assert_eq!(report.trigger_price.unwrap().as_f64(), 49000.0);
981 assert!(report.post_only);
982 assert!(report.reduce_only);
983 }
984
985 #[rstest]
986 fn test_parse_order_status_report_minimal() {
987 let symbol = Ustr::from("ETHUSD");
988 let order = BitmexOrder {
989 account: 0, symbol: Some(Ustr::from("ETHUSD")),
991 order_id: Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap(),
992 cl_ord_id: None,
993 cl_ord_link_id: None,
994 side: Some(BitmexSide::Sell),
995 ord_type: Some(BitmexOrderType::Market),
996 time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
997 ord_status: Some(BitmexOrderStatus::Filled),
998 order_qty: Some(200),
999 cum_qty: Some(200),
1000 price: None,
1001 stop_px: None,
1002 display_qty: None,
1003 peg_offset_value: None,
1004 peg_price_type: None,
1005 currency: None,
1006 settl_currency: None,
1007 exec_inst: None,
1008 contingency_type: None,
1009 ex_destination: None,
1010 triggered: None,
1011 working_indicator: Some(false),
1012 ord_rej_reason: None,
1013 leaves_qty: Some(0),
1014 avg_px: None,
1015 multi_leg_reporting_type: None,
1016 text: None,
1017 transact_time: Some(
1018 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1019 .unwrap()
1020 .with_timezone(&Utc),
1021 ),
1022 timestamp: Some(
1023 DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1024 .unwrap()
1025 .with_timezone(&Utc),
1026 ),
1027 };
1028
1029 let instrument_id = parse_instrument_id(symbol);
1030 let report =
1031 parse_order_status_report(&order, instrument_id, 2, UnixNanos::from(1)).unwrap();
1032
1033 assert_eq!(report.account_id.to_string(), "BITMEX-0");
1034 assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
1035 assert_eq!(
1036 report.venue_order_id.as_str(),
1037 "11111111-2222-3333-4444-555555555555"
1038 );
1039 assert!(report.client_order_id.is_none());
1040 assert_eq!(report.quantity.as_f64(), 200.0);
1041 assert_eq!(report.filled_qty.as_f64(), 200.0);
1042 assert!(report.price.is_none());
1043 assert!(report.trigger_price.is_none());
1044 assert!(!report.post_only);
1045 assert!(!report.reduce_only);
1046 }
1047
1048 #[rstest]
1049 fn test_parse_fill_report() {
1050 let exec = BitmexExecution {
1051 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
1052 account: 654321,
1053 symbol: Some(Ustr::from("XBTUSD")),
1054 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
1055 cl_ord_id: Some(Ustr::from("client-456")),
1056 side: Some(BitmexSide::Buy),
1057 last_qty: 50,
1058 last_px: 50100.5,
1059 commission: Some(0.00075),
1060 settl_currency: Some(Ustr::from("XBt")),
1061 last_liquidity_ind: Some(BitmexLiquidityIndicator::Taker),
1062 trd_match_id: Some(Uuid::parse_str("99999999-8888-7777-6666-555555555555").unwrap()),
1063 transact_time: Some(
1064 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1065 .unwrap()
1066 .with_timezone(&Utc),
1067 ),
1068 cl_ord_link_id: None,
1069 underlying_last_px: None,
1070 last_mkt: None,
1071 order_qty: Some(50),
1072 price: Some(50100.0),
1073 display_qty: None,
1074 stop_px: None,
1075 peg_offset_value: None,
1076 peg_price_type: None,
1077 currency: None,
1078 exec_type: BitmexExecType::Trade,
1079 ord_type: BitmexOrderType::Limit,
1080 time_in_force: BitmexTimeInForce::GoodTillCancel,
1081 exec_inst: None,
1082 contingency_type: None,
1083 ex_destination: None,
1084 ord_status: Some(BitmexOrderStatus::Filled),
1085 triggered: None,
1086 working_indicator: None,
1087 ord_rej_reason: None,
1088 leaves_qty: None,
1089 cum_qty: Some(50),
1090 avg_px: Some(50100.5),
1091 trade_publish_indicator: None,
1092 multi_leg_reporting_type: None,
1093 text: None,
1094 exec_cost: None,
1095 exec_comm: None,
1096 home_notional: None,
1097 foreign_notional: None,
1098 timestamp: None,
1099 };
1100
1101 let report = parse_fill_report(exec, 2, UnixNanos::from(1)).unwrap();
1102
1103 assert_eq!(report.account_id.to_string(), "BITMEX-654321");
1104 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1105 assert_eq!(
1106 report.venue_order_id.as_str(),
1107 "a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6"
1108 );
1109 assert_eq!(
1110 report.trade_id.to_string(),
1111 "99999999-8888-7777-6666-555555555555"
1112 );
1113 assert_eq!(report.client_order_id.unwrap().as_str(), "client-456");
1114 assert_eq!(report.last_qty.as_f64(), 50.0);
1115 assert_eq!(report.last_px.as_f64(), 50100.5);
1116 assert_eq!(report.commission.as_f64(), 0.00075);
1117 assert_eq!(report.commission.currency.code.as_str(), "XBT");
1118 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
1119 }
1120
1121 #[rstest]
1122 fn test_parse_fill_report_with_missing_trd_match_id() {
1123 let exec = BitmexExecution {
1124 exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
1125 account: 111111,
1126 symbol: Some(Ustr::from("ETHUSD")),
1127 order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
1128 cl_ord_id: None,
1129 side: Some(BitmexSide::Sell),
1130 last_qty: 100,
1131 last_px: 3000.0,
1132 commission: None,
1133 settl_currency: None,
1134 last_liquidity_ind: Some(BitmexLiquidityIndicator::Maker),
1135 trd_match_id: None, transact_time: Some(
1137 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1138 .unwrap()
1139 .with_timezone(&Utc),
1140 ),
1141 cl_ord_link_id: None,
1142 underlying_last_px: None,
1143 last_mkt: None,
1144 order_qty: Some(100),
1145 price: Some(3000.0),
1146 display_qty: None,
1147 stop_px: None,
1148 peg_offset_value: None,
1149 peg_price_type: None,
1150 currency: None,
1151 exec_type: BitmexExecType::Trade,
1152 ord_type: BitmexOrderType::Market,
1153 time_in_force: BitmexTimeInForce::ImmediateOrCancel,
1154 exec_inst: None,
1155 contingency_type: None,
1156 ex_destination: None,
1157 ord_status: Some(BitmexOrderStatus::Filled),
1158 triggered: None,
1159 working_indicator: None,
1160 ord_rej_reason: None,
1161 leaves_qty: None,
1162 cum_qty: Some(100),
1163 avg_px: Some(3000.0),
1164 trade_publish_indicator: None,
1165 multi_leg_reporting_type: None,
1166 text: None,
1167 exec_cost: None,
1168 exec_comm: None,
1169 home_notional: None,
1170 foreign_notional: None,
1171 timestamp: None,
1172 };
1173
1174 let report = parse_fill_report(exec, 2, UnixNanos::from(1)).unwrap();
1175
1176 assert_eq!(report.account_id.to_string(), "BITMEX-111111");
1177 assert_eq!(
1178 report.trade_id.to_string(),
1179 "f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6"
1180 );
1181 assert!(report.client_order_id.is_none());
1182 assert_eq!(report.commission.as_f64(), 0.0);
1183 assert_eq!(report.commission.currency.code.as_str(), "XBT");
1184 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
1185 }
1186
1187 #[rstest]
1188 fn test_parse_position_report() {
1189 let position = BitmexPosition {
1190 account: 789012,
1191 symbol: Ustr::from("XBTUSD"),
1192 current_qty: Some(1000),
1193 timestamp: Some(
1194 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1195 .unwrap()
1196 .with_timezone(&Utc),
1197 ),
1198 currency: None,
1199 underlying: None,
1200 quote_currency: None,
1201 commission: None,
1202 init_margin_req: None,
1203 maint_margin_req: None,
1204 risk_limit: None,
1205 leverage: None,
1206 cross_margin: None,
1207 deleverage_percentile: None,
1208 rebalanced_pnl: None,
1209 prev_realised_pnl: None,
1210 prev_unrealised_pnl: None,
1211 prev_close_price: None,
1212 opening_timestamp: None,
1213 opening_qty: None,
1214 opening_cost: None,
1215 opening_comm: None,
1216 open_order_buy_qty: None,
1217 open_order_buy_cost: None,
1218 open_order_buy_premium: None,
1219 open_order_sell_qty: None,
1220 open_order_sell_cost: None,
1221 open_order_sell_premium: None,
1222 exec_buy_qty: None,
1223 exec_buy_cost: None,
1224 exec_sell_qty: None,
1225 exec_sell_cost: None,
1226 exec_qty: None,
1227 exec_cost: None,
1228 exec_comm: None,
1229 current_timestamp: None,
1230 current_cost: None,
1231 current_comm: None,
1232 realised_cost: None,
1233 unrealised_cost: None,
1234 gross_open_cost: None,
1235 gross_open_premium: None,
1236 gross_exec_cost: None,
1237 is_open: Some(true),
1238 mark_price: None,
1239 mark_value: None,
1240 risk_value: None,
1241 home_notional: None,
1242 foreign_notional: None,
1243 pos_state: None,
1244 pos_cost: None,
1245 pos_cost2: None,
1246 pos_cross: None,
1247 pos_init: None,
1248 pos_comm: None,
1249 pos_loss: None,
1250 pos_margin: None,
1251 pos_maint: None,
1252 pos_allowance: None,
1253 taxable_margin: None,
1254 init_margin: None,
1255 maint_margin: None,
1256 session_margin: None,
1257 target_excess_margin: None,
1258 var_margin: None,
1259 realised_gross_pnl: None,
1260 realised_tax: None,
1261 realised_pnl: None,
1262 unrealised_gross_pnl: None,
1263 long_bankrupt: None,
1264 short_bankrupt: None,
1265 tax_base: None,
1266 indicative_tax_rate: None,
1267 indicative_tax: None,
1268 unrealised_tax: None,
1269 unrealised_pnl: None,
1270 unrealised_pnl_pcnt: None,
1271 unrealised_roe_pcnt: None,
1272 avg_cost_price: None,
1273 avg_entry_price: None,
1274 break_even_price: None,
1275 margin_call_price: None,
1276 liquidation_price: None,
1277 bankrupt_price: None,
1278 last_price: None,
1279 last_value: None,
1280 };
1281
1282 let report = parse_position_report(position, UnixNanos::from(1)).unwrap();
1283
1284 assert_eq!(report.account_id.to_string(), "BITMEX-789012");
1285 assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1286 assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
1287 assert_eq!(report.quantity.as_f64(), 1000.0);
1288 }
1289
1290 #[rstest]
1291 fn test_parse_position_report_short() {
1292 let position = BitmexPosition {
1293 account: 789012,
1294 symbol: Ustr::from("ETHUSD"),
1295 current_qty: Some(-500),
1296 timestamp: Some(
1297 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1298 .unwrap()
1299 .with_timezone(&Utc),
1300 ),
1301 currency: None,
1302 underlying: None,
1303 quote_currency: None,
1304 commission: None,
1305 init_margin_req: None,
1306 maint_margin_req: None,
1307 risk_limit: None,
1308 leverage: None,
1309 cross_margin: None,
1310 deleverage_percentile: None,
1311 rebalanced_pnl: None,
1312 prev_realised_pnl: None,
1313 prev_unrealised_pnl: None,
1314 prev_close_price: None,
1315 opening_timestamp: None,
1316 opening_qty: None,
1317 opening_cost: None,
1318 opening_comm: None,
1319 open_order_buy_qty: None,
1320 open_order_buy_cost: None,
1321 open_order_buy_premium: None,
1322 open_order_sell_qty: None,
1323 open_order_sell_cost: None,
1324 open_order_sell_premium: None,
1325 exec_buy_qty: None,
1326 exec_buy_cost: None,
1327 exec_sell_qty: None,
1328 exec_sell_cost: None,
1329 exec_qty: None,
1330 exec_cost: None,
1331 exec_comm: None,
1332 current_timestamp: None,
1333 current_cost: None,
1334 current_comm: None,
1335 realised_cost: None,
1336 unrealised_cost: None,
1337 gross_open_cost: None,
1338 gross_open_premium: None,
1339 gross_exec_cost: None,
1340 is_open: Some(true),
1341 mark_price: None,
1342 mark_value: None,
1343 risk_value: None,
1344 home_notional: None,
1345 foreign_notional: None,
1346 pos_state: None,
1347 pos_cost: None,
1348 pos_cost2: None,
1349 pos_cross: None,
1350 pos_init: None,
1351 pos_comm: None,
1352 pos_loss: None,
1353 pos_margin: None,
1354 pos_maint: None,
1355 pos_allowance: None,
1356 taxable_margin: None,
1357 init_margin: None,
1358 maint_margin: None,
1359 session_margin: None,
1360 target_excess_margin: None,
1361 var_margin: None,
1362 realised_gross_pnl: None,
1363 realised_tax: None,
1364 realised_pnl: None,
1365 unrealised_gross_pnl: None,
1366 long_bankrupt: None,
1367 short_bankrupt: None,
1368 tax_base: None,
1369 indicative_tax_rate: None,
1370 indicative_tax: None,
1371 unrealised_tax: None,
1372 unrealised_pnl: None,
1373 unrealised_pnl_pcnt: None,
1374 unrealised_roe_pcnt: None,
1375 avg_cost_price: None,
1376 avg_entry_price: None,
1377 break_even_price: None,
1378 margin_call_price: None,
1379 liquidation_price: None,
1380 bankrupt_price: None,
1381 last_price: None,
1382 last_value: None,
1383 };
1384
1385 let report = parse_position_report(position, UnixNanos::from(1)).unwrap();
1386
1387 assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
1388 assert_eq!(report.quantity.as_f64(), 500.0); }
1390
1391 #[rstest]
1392 fn test_parse_position_report_flat() {
1393 let position = BitmexPosition {
1394 account: 789012,
1395 symbol: Ustr::from("SOLUSD"),
1396 current_qty: Some(0),
1397 timestamp: Some(
1398 DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1399 .unwrap()
1400 .with_timezone(&Utc),
1401 ),
1402 currency: None,
1403 underlying: None,
1404 quote_currency: None,
1405 commission: None,
1406 init_margin_req: None,
1407 maint_margin_req: None,
1408 risk_limit: None,
1409 leverage: None,
1410 cross_margin: None,
1411 deleverage_percentile: None,
1412 rebalanced_pnl: None,
1413 prev_realised_pnl: None,
1414 prev_unrealised_pnl: None,
1415 prev_close_price: None,
1416 opening_timestamp: None,
1417 opening_qty: None,
1418 opening_cost: None,
1419 opening_comm: None,
1420 open_order_buy_qty: None,
1421 open_order_buy_cost: None,
1422 open_order_buy_premium: None,
1423 open_order_sell_qty: None,
1424 open_order_sell_cost: None,
1425 open_order_sell_premium: None,
1426 exec_buy_qty: None,
1427 exec_buy_cost: None,
1428 exec_sell_qty: None,
1429 exec_sell_cost: None,
1430 exec_qty: None,
1431 exec_cost: None,
1432 exec_comm: None,
1433 current_timestamp: None,
1434 current_cost: None,
1435 current_comm: None,
1436 realised_cost: None,
1437 unrealised_cost: None,
1438 gross_open_cost: None,
1439 gross_open_premium: None,
1440 gross_exec_cost: None,
1441 is_open: Some(true),
1442 mark_price: None,
1443 mark_value: None,
1444 risk_value: None,
1445 home_notional: None,
1446 foreign_notional: None,
1447 pos_state: None,
1448 pos_cost: None,
1449 pos_cost2: None,
1450 pos_cross: None,
1451 pos_init: None,
1452 pos_comm: None,
1453 pos_loss: None,
1454 pos_margin: None,
1455 pos_maint: None,
1456 pos_allowance: None,
1457 taxable_margin: None,
1458 init_margin: None,
1459 maint_margin: None,
1460 session_margin: None,
1461 target_excess_margin: None,
1462 var_margin: None,
1463 realised_gross_pnl: None,
1464 realised_tax: None,
1465 realised_pnl: None,
1466 unrealised_gross_pnl: None,
1467 long_bankrupt: None,
1468 short_bankrupt: None,
1469 tax_base: None,
1470 indicative_tax_rate: None,
1471 indicative_tax: None,
1472 unrealised_tax: None,
1473 unrealised_pnl: None,
1474 unrealised_pnl_pcnt: None,
1475 unrealised_roe_pcnt: None,
1476 avg_cost_price: None,
1477 avg_entry_price: None,
1478 break_even_price: None,
1479 margin_call_price: None,
1480 liquidation_price: None,
1481 bankrupt_price: None,
1482 last_price: None,
1483 last_value: None,
1484 };
1485
1486 let report = parse_position_report(position, UnixNanos::from(1)).unwrap();
1487
1488 assert_eq!(report.position_side.as_position_side(), PositionSide::Flat);
1489 assert_eq!(report.quantity.as_f64(), 0.0);
1490 }
1491
1492 fn create_test_spot_instrument() -> BitmexInstrument {
1497 BitmexInstrument {
1498 symbol: Ustr::from("XBTUSD"),
1499 root_symbol: Ustr::from("XBT"),
1500 state: BitmexInstrumentState::Open,
1501 instrument_type: BitmexInstrumentType::Spot,
1502 listing: DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
1503 .unwrap()
1504 .with_timezone(&Utc),
1505 front: Some(
1506 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
1507 .unwrap()
1508 .with_timezone(&Utc),
1509 ),
1510 expiry: None,
1511 settle: None,
1512 listed_settle: None,
1513 position_currency: Some(Ustr::from("USD")),
1514 underlying: Ustr::from("XBT"),
1515 quote_currency: Ustr::from("USD"),
1516 underlying_symbol: Some(Ustr::from("XBT=")),
1517 reference: Some(Ustr::from("BMEX")),
1518 reference_symbol: Some(Ustr::from(".BXBT")),
1519 lot_size: Some(1.0),
1520 tick_size: 0.01,
1521 multiplier: 1.0,
1522 settl_currency: Some(Ustr::from("USD")),
1523 is_quanto: false,
1524 is_inverse: false,
1525 maker_fee: Some(-0.00025),
1526 taker_fee: Some(0.00075),
1527 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
1528 .unwrap()
1529 .with_timezone(&Utc),
1530 max_order_qty: Some(10000000.0),
1532 max_price: Some(1000000.0),
1533 settlement_fee: Some(0.0),
1534 mark_price: Some(50500.0),
1535 last_price: Some(50500.0),
1536 bid_price: Some(50499.5),
1537 ask_price: Some(50500.5),
1538 open_interest: Some(0.0),
1539 open_value: Some(0.0),
1540 total_volume: Some(1000000.0),
1541 volume: Some(50000.0),
1542 volume_24h: Some(75000.0),
1543 total_turnover: Some(150000000.0),
1544 turnover: Some(5000000.0),
1545 turnover_24h: Some(7500000.0),
1546 has_liquidity: Some(true),
1547 calc_interval: None,
1549 publish_interval: None,
1550 publish_time: None,
1551 underlying_to_position_multiplier: Some(1.0),
1552 underlying_to_settle_multiplier: None,
1553 quote_to_settle_multiplier: Some(1.0),
1554 init_margin: Some(0.1),
1555 maint_margin: Some(0.05),
1556 risk_limit: Some(20000000000.0),
1557 risk_step: Some(10000000000.0),
1558 limit: None,
1559 taxed: Some(true),
1560 deleverage: Some(true),
1561 funding_base_symbol: None,
1562 funding_quote_symbol: None,
1563 funding_premium_symbol: None,
1564 funding_timestamp: None,
1565 funding_interval: None,
1566 funding_rate: None,
1567 indicative_funding_rate: None,
1568 rebalance_timestamp: None,
1569 rebalance_interval: None,
1570 prev_close_price: Some(50000.0),
1571 limit_down_price: None,
1572 limit_up_price: None,
1573 prev_total_turnover: Some(100000000.0),
1574 home_notional_24h: Some(1.5),
1575 foreign_notional_24h: Some(75000.0),
1576 prev_price_24h: Some(49500.0),
1577 vwap: Some(50100.0),
1578 high_price: Some(51000.0),
1579 low_price: Some(49000.0),
1580 last_price_protected: Some(50500.0),
1581 last_tick_direction: Some(BitmexTickDirection::PlusTick),
1582 last_change_pcnt: Some(0.0202),
1583 mid_price: Some(50500.0),
1584 impact_bid_price: Some(50490.0),
1585 impact_mid_price: Some(50495.0),
1586 impact_ask_price: Some(50500.0),
1587 fair_method: None,
1588 fair_basis_rate: None,
1589 fair_basis: None,
1590 fair_price: None,
1591 mark_method: Some(BitmexMarkMethod::LastPrice),
1592 indicative_settle_price: None,
1593 settled_price_adjustment_rate: None,
1594 settled_price: None,
1595 instant_pnl: false,
1596 min_tick: None,
1597 funding_base_rate: None,
1598 funding_quote_rate: None,
1599 }
1600 }
1601
1602 fn create_test_perpetual_instrument() -> BitmexInstrument {
1603 BitmexInstrument {
1604 symbol: Ustr::from("XBTUSD"),
1605 root_symbol: Ustr::from("XBT"),
1606 state: BitmexInstrumentState::Open,
1607 instrument_type: BitmexInstrumentType::PerpetualContract,
1608 listing: DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
1609 .unwrap()
1610 .with_timezone(&Utc),
1611 front: Some(
1612 DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
1613 .unwrap()
1614 .with_timezone(&Utc),
1615 ),
1616 expiry: None,
1617 settle: None,
1618 listed_settle: None,
1619 position_currency: Some(Ustr::from("USD")),
1620 underlying: Ustr::from("XBT"),
1621 quote_currency: Ustr::from("USD"),
1622 underlying_symbol: Some(Ustr::from("XBT=")),
1623 reference: Some(Ustr::from("BMEX")),
1624 reference_symbol: Some(Ustr::from(".BXBT")),
1625 lot_size: Some(1.0),
1626 tick_size: 0.5,
1627 multiplier: -1.0,
1628 settl_currency: Some(Ustr::from("XBT")),
1629 is_quanto: false,
1630 is_inverse: true,
1631 maker_fee: Some(-0.00025),
1632 taker_fee: Some(0.00075),
1633 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
1634 .unwrap()
1635 .with_timezone(&Utc),
1636 max_order_qty: Some(10000000.0),
1638 max_price: Some(1000000.0),
1639 settlement_fee: Some(0.0),
1640 mark_price: Some(50500.01),
1641 last_price: Some(50500.0),
1642 bid_price: Some(50499.5),
1643 ask_price: Some(50500.5),
1644 open_interest: Some(500000000.0),
1645 open_value: Some(990099009900.0),
1646 total_volume: Some(12345678900000.0),
1647 volume: Some(5000000.0),
1648 volume_24h: Some(75000000.0),
1649 total_turnover: Some(150000000000000.0),
1650 turnover: Some(5000000000.0),
1651 turnover_24h: Some(7500000000.0),
1652 has_liquidity: Some(true),
1653 funding_base_symbol: Some(Ustr::from(".XBTBON8H")),
1655 funding_quote_symbol: Some(Ustr::from(".USDBON8H")),
1656 funding_premium_symbol: Some(Ustr::from(".XBTUSDPI8H")),
1657 funding_timestamp: Some(
1658 DateTime::parse_from_rfc3339("2024-01-01T08:00:00.000Z")
1659 .unwrap()
1660 .with_timezone(&Utc),
1661 ),
1662 funding_interval: Some(
1663 DateTime::parse_from_rfc3339("2000-01-01T08:00:00.000Z")
1664 .unwrap()
1665 .with_timezone(&Utc),
1666 ),
1667 funding_rate: Some(0.0001),
1668 indicative_funding_rate: Some(0.0001),
1669 funding_base_rate: Some(0.01),
1670 funding_quote_rate: Some(-0.01),
1671 calc_interval: None,
1673 publish_interval: None,
1674 publish_time: None,
1675 underlying_to_position_multiplier: Some(1.0),
1676 underlying_to_settle_multiplier: None,
1677 quote_to_settle_multiplier: Some(0.00000001),
1678 init_margin: Some(0.01),
1679 maint_margin: Some(0.005),
1680 risk_limit: Some(20000000000.0),
1681 risk_step: Some(10000000000.0),
1682 limit: None,
1683 taxed: Some(true),
1684 deleverage: Some(true),
1685 rebalance_timestamp: None,
1686 rebalance_interval: None,
1687 prev_close_price: Some(50000.0),
1688 limit_down_price: None,
1689 limit_up_price: None,
1690 prev_total_turnover: Some(100000000000000.0),
1691 home_notional_24h: Some(1500.0),
1692 foreign_notional_24h: Some(75000000.0),
1693 prev_price_24h: Some(49500.0),
1694 vwap: Some(50100.0),
1695 high_price: Some(51000.0),
1696 low_price: Some(49000.0),
1697 last_price_protected: Some(50500.0),
1698 last_tick_direction: Some(BitmexTickDirection::PlusTick),
1699 last_change_pcnt: Some(0.0202),
1700 mid_price: Some(50500.0),
1701 impact_bid_price: Some(50490.0),
1702 impact_mid_price: Some(50495.0),
1703 impact_ask_price: Some(50500.0),
1704 fair_method: Some(BitmexFairMethod::FundingRate),
1705 fair_basis_rate: Some(0.1095),
1706 fair_basis: Some(0.01),
1707 fair_price: Some(50500.01),
1708 mark_method: Some(BitmexMarkMethod::FairPrice),
1709 indicative_settle_price: Some(50500.0),
1710 settled_price_adjustment_rate: None,
1711 settled_price: None,
1712 instant_pnl: false,
1713 min_tick: None,
1714 }
1715 }
1716
1717 fn create_test_futures_instrument() -> BitmexInstrument {
1718 BitmexInstrument {
1719 symbol: Ustr::from("XBTH25"),
1720 root_symbol: Ustr::from("XBT"),
1721 state: BitmexInstrumentState::Open,
1722 instrument_type: BitmexInstrumentType::Futures,
1723 listing: DateTime::parse_from_rfc3339("2024-09-27T12:00:00.000Z")
1724 .unwrap()
1725 .with_timezone(&Utc),
1726 front: Some(
1727 DateTime::parse_from_rfc3339("2024-12-27T12:00:00.000Z")
1728 .unwrap()
1729 .with_timezone(&Utc),
1730 ),
1731 expiry: Some(
1732 DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
1733 .unwrap()
1734 .with_timezone(&Utc),
1735 ),
1736 settle: Some(
1737 DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
1738 .unwrap()
1739 .with_timezone(&Utc),
1740 ),
1741 listed_settle: None,
1742 position_currency: Some(Ustr::from("USD")),
1743 underlying: Ustr::from("XBT"),
1744 quote_currency: Ustr::from("USD"),
1745 underlying_symbol: Some(Ustr::from("XBT=")),
1746 reference: Some(Ustr::from("BMEX")),
1747 reference_symbol: Some(Ustr::from(".BXBT30M")),
1748 lot_size: Some(1.0),
1749 tick_size: 0.5,
1750 multiplier: -1.0,
1751 settl_currency: Some(Ustr::from("XBT")),
1752 is_quanto: false,
1753 is_inverse: true,
1754 maker_fee: Some(-0.00025),
1755 taker_fee: Some(0.00075),
1756 settlement_fee: Some(0.0005),
1757 timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
1758 .unwrap()
1759 .with_timezone(&Utc),
1760 max_order_qty: Some(10000000.0),
1762 max_price: Some(1000000.0),
1763 mark_price: Some(55500.0),
1764 last_price: Some(55500.0),
1765 bid_price: Some(55499.5),
1766 ask_price: Some(55500.5),
1767 open_interest: Some(50000000.0),
1768 open_value: Some(90090090090.0),
1769 total_volume: Some(1000000000.0),
1770 volume: Some(500000.0),
1771 volume_24h: Some(7500000.0),
1772 total_turnover: Some(15000000000000.0),
1773 turnover: Some(500000000.0),
1774 turnover_24h: Some(750000000.0),
1775 has_liquidity: Some(true),
1776 funding_base_symbol: None,
1778 funding_quote_symbol: None,
1779 funding_premium_symbol: None,
1780 funding_timestamp: None,
1781 funding_interval: None,
1782 funding_rate: None,
1783 indicative_funding_rate: None,
1784 funding_base_rate: None,
1785 funding_quote_rate: None,
1786 calc_interval: None,
1788 publish_interval: None,
1789 publish_time: None,
1790 underlying_to_position_multiplier: Some(1.0),
1791 underlying_to_settle_multiplier: None,
1792 quote_to_settle_multiplier: Some(0.00000001),
1793 init_margin: Some(0.02),
1794 maint_margin: Some(0.01),
1795 risk_limit: Some(20000000000.0),
1796 risk_step: Some(10000000000.0),
1797 limit: None,
1798 taxed: Some(true),
1799 deleverage: Some(true),
1800 rebalance_timestamp: None,
1801 rebalance_interval: None,
1802 prev_close_price: Some(55000.0),
1803 limit_down_price: None,
1804 limit_up_price: None,
1805 prev_total_turnover: Some(10000000000000.0),
1806 home_notional_24h: Some(150.0),
1807 foreign_notional_24h: Some(7500000.0),
1808 prev_price_24h: Some(54500.0),
1809 vwap: Some(55100.0),
1810 high_price: Some(56000.0),
1811 low_price: Some(54000.0),
1812 last_price_protected: Some(55500.0),
1813 last_tick_direction: Some(BitmexTickDirection::PlusTick),
1814 last_change_pcnt: Some(0.0183),
1815 mid_price: Some(55500.0),
1816 impact_bid_price: Some(55490.0),
1817 impact_mid_price: Some(55495.0),
1818 impact_ask_price: Some(55500.0),
1819 fair_method: Some(BitmexFairMethod::ImpactMidPrice),
1820 fair_basis_rate: Some(1.8264),
1821 fair_basis: Some(1000.0),
1822 fair_price: Some(55500.0),
1823 mark_method: Some(BitmexMarkMethod::FairPrice),
1824 indicative_settle_price: Some(55500.0),
1825 settled_price_adjustment_rate: None,
1826 settled_price: None,
1827 instant_pnl: false,
1828 min_tick: None,
1829 }
1830 }
1831
1832 #[rstest]
1837 fn test_parse_spot_instrument() {
1838 let instrument = create_test_spot_instrument();
1839 let ts_init = UnixNanos::default();
1840 let result = parse_spot_instrument(&instrument, ts_init).unwrap();
1841
1842 match result {
1844 nautilus_model::instruments::InstrumentAny::CurrencyPair(spot) => {
1845 assert_eq!(spot.id.symbol.as_str(), "XBTUSD");
1846 assert_eq!(spot.id.venue.as_str(), "BITMEX");
1847 assert_eq!(spot.raw_symbol.as_str(), "XBTUSD");
1848 assert_eq!(spot.price_precision, 2);
1849 assert_eq!(spot.size_precision, 0);
1850 assert_eq!(spot.price_increment.as_f64(), 0.01);
1851 assert_eq!(spot.size_increment.as_f64(), 1.0);
1852 assert_eq!(spot.maker_fee.to_f64().unwrap(), -0.00025);
1853 assert_eq!(spot.taker_fee.to_f64().unwrap(), 0.00075);
1854 }
1855 _ => panic!("Expected CurrencyPair variant"),
1856 }
1857 }
1858
1859 #[rstest]
1860 fn test_parse_perpetual_instrument() {
1861 let instrument = create_test_perpetual_instrument();
1862 let ts_init = UnixNanos::default();
1863 let result = parse_perpetual_instrument(&instrument, ts_init).unwrap();
1864
1865 match result {
1867 nautilus_model::instruments::InstrumentAny::CryptoPerpetual(perp) => {
1868 assert_eq!(perp.id.symbol.as_str(), "XBTUSD");
1869 assert_eq!(perp.id.venue.as_str(), "BITMEX");
1870 assert_eq!(perp.raw_symbol.as_str(), "XBTUSD");
1871 assert_eq!(perp.price_precision, 1);
1872 assert_eq!(perp.size_precision, 0);
1873 assert_eq!(perp.price_increment.as_f64(), 0.5);
1874 assert_eq!(perp.size_increment.as_f64(), 1.0);
1875 assert_eq!(perp.maker_fee.to_f64().unwrap(), -0.00025);
1876 assert_eq!(perp.taker_fee.to_f64().unwrap(), 0.00075);
1877 assert!(perp.is_inverse);
1878 }
1879 _ => panic!("Expected CryptoPerpetual variant"),
1880 }
1881 }
1882
1883 #[rstest]
1884 fn test_parse_futures_instrument() {
1885 let instrument = create_test_futures_instrument();
1886 let ts_init = UnixNanos::default();
1887 let result = parse_futures_instrument(&instrument, ts_init).unwrap();
1888
1889 match result {
1891 nautilus_model::instruments::InstrumentAny::CryptoFuture(instrument) => {
1892 assert_eq!(instrument.id.symbol.as_str(), "XBTH25");
1893 assert_eq!(instrument.id.venue.as_str(), "BITMEX");
1894 assert_eq!(instrument.raw_symbol.as_str(), "XBTH25");
1895 assert_eq!(instrument.underlying.code.as_str(), "XBT");
1896 assert_eq!(instrument.price_precision, 1);
1897 assert_eq!(instrument.size_precision, 0);
1898 assert_eq!(instrument.price_increment.as_f64(), 0.5);
1899 assert_eq!(instrument.size_increment.as_f64(), 1.0);
1900 assert_eq!(instrument.maker_fee.to_f64().unwrap(), -0.00025);
1901 assert_eq!(instrument.taker_fee.to_f64().unwrap(), 0.00075);
1902 assert!(instrument.is_inverse);
1903 assert!(instrument.expiration_ns.as_u64() > 0);
1906 }
1907 _ => panic!("Expected CryptoFuture variant"),
1908 }
1909 }
1910}