1use chrono::{DateTime, Utc};
19use nautilus_core::{nanos::UnixNanos, uuid::UUID4};
20use nautilus_model::{
21 data::bar::BarType,
22 enums::{AccountType, AggressorSide, CurrencyType, LiquiditySide, PositionSide},
23 events::AccountState,
24 identifiers::{AccountId, InstrumentId, Symbol},
25 instruments::{Instrument, InstrumentAny},
26 types::{
27 AccountBalance, Currency, Money, Price, Quantity,
28 quantity::{QUANTITY_RAW_MAX, QuantityRaw},
29 },
30};
31use rust_decimal::{Decimal, RoundingStrategy, prelude::ToPrimitive};
32use ustr::Ustr;
33
34use crate::{
35 common::{
36 consts::BITMEX_VENUE,
37 enums::{BitmexLiquidityIndicator, BitmexSide},
38 },
39 websocket::messages::BitmexMarginMsg,
40};
41
42#[must_use]
46pub fn clean_reason(reason: &str) -> String {
47 reason.replace("\nNautilusTrader", "").trim().to_string()
48}
49
50#[must_use]
52pub fn parse_instrument_id(symbol: Ustr) -> InstrumentId {
53 InstrumentId::new(Symbol::from_ustr_unchecked(symbol), *BITMEX_VENUE)
54}
55
56#[must_use]
64pub fn quantity_to_u32(quantity: &Quantity, instrument: &InstrumentAny) -> u32 {
65 let size_increment = instrument.size_increment();
66 let step_decimal = size_increment.as_decimal();
67
68 if step_decimal.is_zero() {
69 let value = quantity.as_f64();
70 if value > u32::MAX as f64 {
71 tracing::warn!(
72 "Quantity {value} exceeds u32::MAX without instrument increment, clamping",
73 );
74 return u32::MAX;
75 }
76 return value.max(0.0) as u32;
77 }
78
79 let units_decimal = quantity.as_decimal() / step_decimal;
80 let rounded_units =
81 units_decimal.round_dp_with_strategy(0, RoundingStrategy::MidpointAwayFromZero);
82
83 match rounded_units.to_u128() {
84 Some(units) if units <= u32::MAX as u128 => units as u32,
85 Some(units) => {
86 tracing::warn!(
87 "Quantity {} converts to {units} contracts which exceeds u32::MAX, clamping",
88 quantity.as_f64(),
89 );
90 u32::MAX
91 }
92 None => {
93 tracing::warn!(
94 "Failed to convert quantity {} to venue units, defaulting to 0",
95 quantity.as_f64(),
96 );
97 0
98 }
99 }
100}
101
102#[must_use]
104pub fn parse_contracts_quantity(value: u64, instrument: &InstrumentAny) -> Quantity {
105 let size_increment = instrument.size_increment();
106 let precision = instrument.size_precision();
107
108 let increment_raw: QuantityRaw = (&size_increment).into();
109 let value_raw = QuantityRaw::from(value);
110
111 let mut raw = increment_raw.saturating_mul(value_raw);
112 if raw > QUANTITY_RAW_MAX {
113 tracing::warn!(
114 "Quantity value {value} exceeds QUANTITY_RAW_MAX {}, clamping",
115 QUANTITY_RAW_MAX,
116 );
117 raw = QUANTITY_RAW_MAX;
118 }
119
120 Quantity::from_raw(raw, precision)
121}
122
123pub fn derive_contract_decimal_and_increment(
133 multiplier: Option<f64>,
134 max_scale: u32,
135) -> anyhow::Result<(Decimal, Quantity)> {
136 let raw_multiplier = multiplier.unwrap_or(1.0);
137 let contract_size = if raw_multiplier > 0.0 {
138 1.0 / raw_multiplier
139 } else {
140 1.0
141 };
142
143 let mut contract_decimal = Decimal::from_f64_retain(contract_size)
144 .ok_or_else(|| anyhow::anyhow!("Invalid contract size {contract_size}"))?;
145 if contract_decimal.scale() > max_scale {
146 contract_decimal = contract_decimal
147 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
148 }
149 contract_decimal = contract_decimal.normalize();
150 let contract_precision = contract_decimal.scale() as u8;
151 let size_increment = Quantity::from_decimal(contract_decimal, contract_precision)?;
152
153 Ok((contract_decimal, size_increment))
154}
155
156pub fn convert_contract_quantity(
163 value: Option<f64>,
164 contract_decimal: Decimal,
165 max_scale: u32,
166 field_name: &str,
167) -> anyhow::Result<Option<Quantity>> {
168 value
169 .map(|raw| {
170 let mut decimal = Decimal::from_f64_retain(raw)
171 .ok_or_else(|| anyhow::anyhow!("Invalid {field_name} value"))?
172 * contract_decimal;
173 let scale = decimal.scale();
174 if scale > max_scale {
175 decimal = decimal
176 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
177 }
178 let decimal = decimal.normalize();
179 let precision = decimal.scale() as u8;
180 Quantity::from_decimal(decimal, precision)
181 })
182 .transpose()
183}
184
185#[must_use]
187pub fn parse_signed_contracts_quantity(value: i64, instrument: &InstrumentAny) -> Quantity {
188 let abs_value = value.checked_abs().unwrap_or_else(|| {
189 tracing::warn!("Quantity value {value} overflowed when taking absolute value");
190 i64::MAX
191 }) as u64;
192 parse_contracts_quantity(abs_value, instrument)
193}
194
195#[must_use]
197pub fn parse_fractional_quantity(value: f64, instrument: &InstrumentAny) -> Quantity {
198 if value < 0.0 {
199 tracing::warn!("Received negative fractional quantity {value}, defaulting to 0.0");
200 return instrument.make_qty(0.0, None);
201 }
202
203 instrument.try_make_qty(value, None).unwrap_or_else(|err| {
204 tracing::warn!(
205 "Failed to convert fractional quantity {value} with precision {}: {err}",
206 instrument.size_precision(),
207 );
208 instrument.make_qty(0.0, None)
209 })
210}
211
212#[must_use]
220pub fn normalize_trade_bin_prices(
221 open: Price,
222 mut high: Price,
223 mut low: Price,
224 close: Price,
225 symbol: &Ustr,
226 bar_type: Option<&BarType>,
227) -> (Price, Price, Price, Price) {
228 let price_extremes = [open, high, low, close];
229 let max_price = *price_extremes
230 .iter()
231 .max()
232 .expect("Price array contains values");
233 let min_price = *price_extremes
234 .iter()
235 .min()
236 .expect("Price array contains values");
237
238 if high < max_price || low > min_price {
239 match bar_type {
240 Some(bt) => {
241 tracing::warn!(symbol = %symbol, ?bt, "Adjusting BitMEX trade bin extremes")
242 }
243 None => tracing::warn!(symbol = %symbol, "Adjusting BitMEX trade bin extremes"),
244 }
245 high = max_price;
246 low = min_price;
247 }
248
249 (open, high, low, close)
250}
251
252#[must_use]
255pub fn normalize_trade_bin_volume(volume: Option<i64>, symbol: &Ustr) -> u64 {
256 match volume {
257 Some(v) if v >= 0 => v as u64,
258 Some(v) => {
259 tracing::warn!(symbol = %symbol, volume = v, "Received negative volume in BitMEX trade bin");
260 0
261 }
262 None => {
263 tracing::warn!(symbol = %symbol, "Trade bin missing volume, defaulting to 0");
264 0
265 }
266 }
267}
268
269#[must_use]
274pub fn parse_optional_datetime_to_unix_nanos(
275 value: &Option<DateTime<Utc>>,
276 field: &str,
277) -> UnixNanos {
278 value
279 .map(|dt| {
280 UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or_else(|| {
281 tracing::error!(field = field, timestamp = ?dt, "Invalid timestamp - out of range");
282 0
283 }) as u64)
284 })
285 .unwrap_or_default()
286}
287
288#[must_use]
290pub const fn parse_aggressor_side(side: &Option<BitmexSide>) -> AggressorSide {
291 match side {
292 Some(BitmexSide::Buy) => AggressorSide::Buyer,
293 Some(BitmexSide::Sell) => AggressorSide::Seller,
294 None => AggressorSide::NoAggressor,
295 }
296}
297
298#[must_use]
300pub fn parse_liquidity_side(liquidity: &Option<BitmexLiquidityIndicator>) -> LiquiditySide {
301 liquidity
302 .map(std::convert::Into::into)
303 .unwrap_or(LiquiditySide::NoLiquiditySide)
304}
305
306#[must_use]
308pub const fn parse_position_side(current_qty: Option<i64>) -> PositionSide {
309 match current_qty {
310 Some(qty) if qty > 0 => PositionSide::Long,
311 Some(qty) if qty < 0 => PositionSide::Short,
312 _ => PositionSide::Flat,
313 }
314}
315
316#[must_use]
327pub fn map_bitmex_currency(bitmex_currency: &str) -> String {
328 match bitmex_currency {
329 "XBt" => "XBT".to_string(),
330 "USDt" => "USDT".to_string(),
331 "LAMp" => "USDT".to_string(), "RLUSd" => "RLUSD".to_string(),
333 "MAMUSd" => "MAMUSD".to_string(),
334 other => other.to_uppercase(),
335 }
336}
337
338pub fn parse_account_state(
344 margin: &BitmexMarginMsg,
345 account_id: AccountId,
346 ts_init: UnixNanos,
347) -> anyhow::Result<AccountState> {
348 tracing::debug!(
349 "Parsing margin: currency={}, wallet_balance={:?}, available_margin={:?}, init_margin={:?}, maint_margin={:?}, foreign_margin_balance={:?}, foreign_requirement={:?}",
350 margin.currency,
351 margin.wallet_balance,
352 margin.available_margin,
353 margin.init_margin,
354 margin.maint_margin,
355 margin.foreign_margin_balance,
356 margin.foreign_requirement
357 );
358
359 let currency_str = map_bitmex_currency(&margin.currency);
360
361 let currency = match Currency::try_from_str(¤cy_str) {
362 Some(c) => c,
363 None => {
364 tracing::warn!(
366 "Unknown currency '{currency_str}' in margin message, creating default crypto currency"
367 );
368 Currency::new(¤cy_str, 8, 0, ¤cy_str, CurrencyType::Crypto)
369 }
370 };
371
372 let divisor = if margin.currency == "XBt" {
374 100_000_000.0 } else if margin.currency == "USDt" || margin.currency == "LAMp" {
376 1_000_000.0 } else {
378 1.0
379 };
380
381 let total = if let Some(wallet_balance) = margin.wallet_balance {
383 Money::new(wallet_balance as f64 / divisor, currency)
384 } else if let Some(margin_balance) = margin.margin_balance {
385 Money::new(margin_balance as f64 / divisor, currency)
386 } else if let Some(available) = margin.available_margin {
387 Money::new(available as f64 / divisor, currency)
389 } else {
390 Money::new(0.0, currency)
391 };
392
393 let margin_used = if let Some(init_margin) = margin.init_margin {
395 Money::new(init_margin as f64 / divisor, currency)
396 } else {
397 Money::new(0.0, currency)
398 };
399
400 let free = if let Some(withdrawable) = margin.withdrawable_margin {
402 Money::new(withdrawable as f64 / divisor, currency)
403 } else if let Some(available) = margin.available_margin {
404 let available_money = Money::new(available as f64 / divisor, currency);
406 if available_money > total {
408 total
409 } else {
410 available_money
411 }
412 } else {
413 let calculated_free = total - margin_used;
415 if calculated_free < Money::new(0.0, currency) {
416 Money::new(0.0, currency)
417 } else {
418 calculated_free
419 }
420 };
421
422 let locked = total - free;
424
425 let balance = AccountBalance::new(total, locked, free);
426 let balances = vec![balance];
427
428 let margins = Vec::new();
431
432 let account_type = AccountType::Margin;
433 let is_reported = true;
434 let event_id = UUID4::new();
435 let ts_event =
436 UnixNanos::from(margin.timestamp.timestamp_nanos_opt().unwrap_or_default() as u64);
437
438 Ok(AccountState::new(
439 account_id,
440 account_type,
441 balances,
442 margins,
443 is_reported,
444 event_id,
445 ts_event,
446 ts_init,
447 None,
448 ))
449}
450
451#[cfg(test)]
456mod tests {
457 use chrono::TimeZone;
458 use nautilus_model::{instruments::CurrencyPair, types::fixed::FIXED_PRECISION};
459 use rstest::rstest;
460 use ustr::Ustr;
461
462 use super::*;
463
464 #[rstest]
465 fn test_clean_reason_strips_nautilus_trader() {
466 assert_eq!(
467 clean_reason(
468 "Canceled: Order had execInst of ParticipateDoNotInitiate\nNautilusTrader"
469 ),
470 "Canceled: Order had execInst of ParticipateDoNotInitiate"
471 );
472
473 assert_eq!(clean_reason("Some error\nNautilusTrader"), "Some error");
474 assert_eq!(
475 clean_reason("Multiple lines\nSome content\nNautilusTrader"),
476 "Multiple lines\nSome content"
477 );
478 assert_eq!(clean_reason("No identifier here"), "No identifier here");
479 assert_eq!(clean_reason(" \nNautilusTrader "), "");
480 }
481
482 fn make_test_spot_instrument(size_increment: f64, size_precision: u8) -> InstrumentAny {
483 let instrument_id = InstrumentId::from("SOLUSDT.BITMEX");
484 let raw_symbol = Symbol::from("SOLUSDT");
485 let base_currency = Currency::from("SOL");
486 let quote_currency = Currency::from("USDT");
487 let price_precision = 2;
488 let price_increment = Price::new(0.01, price_precision);
489 let size_increment = Quantity::new(size_increment, size_precision);
490 let instrument = CurrencyPair::new(
491 instrument_id,
492 raw_symbol,
493 base_currency,
494 quote_currency,
495 price_precision,
496 size_precision,
497 price_increment,
498 size_increment,
499 None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::from(0),
512 UnixNanos::from(0),
513 );
514 InstrumentAny::CurrencyPair(instrument)
515 }
516
517 #[rstest]
518 fn test_quantity_to_u32_scaled() {
519 let instrument = make_test_spot_instrument(0.0001, 4);
520 let qty = Quantity::new(0.1, 4);
521 assert_eq!(quantity_to_u32(&qty, &instrument), 1_000);
522 }
523
524 #[rstest]
525 fn test_parse_contracts_quantity_scaled() {
526 let instrument = make_test_spot_instrument(0.0001, 4);
527 let qty = parse_contracts_quantity(1_000, &instrument);
528 assert!((qty.as_f64() - 0.1).abs() < 1e-9);
529 assert_eq!(qty.precision, 4);
530 }
531
532 #[rstest]
533 fn test_convert_contract_quantity_scaling() {
534 let max_scale = FIXED_PRECISION as u32;
535 let (contract_decimal, size_increment) =
536 derive_contract_decimal_and_increment(Some(10_000.0), max_scale).unwrap();
537 assert!((size_increment.as_f64() - 0.0001).abs() < 1e-12);
538
539 let lot_qty =
540 convert_contract_quantity(Some(1_000.0), contract_decimal, max_scale, "lot size")
541 .unwrap()
542 .unwrap();
543 assert!((lot_qty.as_f64() - 0.1).abs() < 1e-9);
544 assert_eq!(lot_qty.precision, 1);
545 }
546
547 #[rstest]
548 fn test_derive_contract_decimal_defaults_to_one() {
549 let max_scale = FIXED_PRECISION as u32;
550 let (contract_decimal, size_increment) =
551 derive_contract_decimal_and_increment(Some(0.0), max_scale).unwrap();
552 assert_eq!(contract_decimal, Decimal::ONE);
553 assert_eq!(size_increment.as_f64(), 1.0);
554 }
555
556 #[rstest]
557 fn test_parse_account_state() {
558 let margin_msg = BitmexMarginMsg {
559 account: 123456,
560 currency: Ustr::from("XBt"),
561 risk_limit: Some(1000000000),
562 amount: Some(5000000),
563 prev_realised_pnl: Some(100000),
564 gross_comm: Some(1000),
565 gross_open_cost: Some(200000),
566 gross_open_premium: None,
567 gross_exec_cost: None,
568 gross_mark_value: Some(210000),
569 risk_value: Some(50000),
570 init_margin: Some(20000),
571 maint_margin: Some(10000),
572 target_excess_margin: Some(5000),
573 realised_pnl: Some(100000),
574 unrealised_pnl: Some(10000),
575 wallet_balance: Some(5000000),
576 margin_balance: Some(5010000),
577 margin_leverage: Some(2.5),
578 margin_used_pcnt: Some(0.25),
579 excess_margin: Some(4990000),
580 available_margin: Some(4980000),
581 withdrawable_margin: Some(4900000),
582 maker_fee_discount: Some(0.1),
583 taker_fee_discount: Some(0.05),
584 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
585 foreign_margin_balance: None,
586 foreign_requirement: None,
587 };
588
589 let account_id = AccountId::new("BITMEX-001");
590 let ts_init = UnixNanos::from(1_000_000_000);
591
592 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
593
594 assert_eq!(account_state.account_id, account_id);
595 assert_eq!(account_state.account_type, AccountType::Margin);
596 assert_eq!(account_state.balances.len(), 1);
597 assert_eq!(account_state.margins.len(), 0); assert!(account_state.is_reported);
599
600 let xbt_balance = &account_state.balances[0];
601 assert_eq!(xbt_balance.currency, Currency::from("XBT"));
602 assert_eq!(xbt_balance.total.as_f64(), 0.05); assert_eq!(xbt_balance.free.as_f64(), 0.049); assert_eq!(xbt_balance.locked.as_f64(), 0.001); }
606
607 #[rstest]
608 fn test_parse_account_state_usdt() {
609 let margin_msg = BitmexMarginMsg {
610 account: 123456,
611 currency: Ustr::from("USDt"),
612 risk_limit: Some(1000000000),
613 amount: Some(10000000000), prev_realised_pnl: None,
615 gross_comm: None,
616 gross_open_cost: None,
617 gross_open_premium: None,
618 gross_exec_cost: None,
619 gross_mark_value: None,
620 risk_value: None,
621 init_margin: Some(500000), maint_margin: Some(250000), target_excess_margin: None,
624 realised_pnl: None,
625 unrealised_pnl: None,
626 wallet_balance: Some(10000000000),
627 margin_balance: Some(10000000000),
628 margin_leverage: None,
629 margin_used_pcnt: None,
630 excess_margin: None,
631 available_margin: Some(9500000000), withdrawable_margin: None,
633 maker_fee_discount: None,
634 taker_fee_discount: None,
635 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
636 foreign_margin_balance: None,
637 foreign_requirement: None,
638 };
639
640 let account_id = AccountId::new("BITMEX-001");
641 let ts_init = UnixNanos::from(1_000_000_000);
642
643 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
644
645 let usdt_balance = &account_state.balances[0];
646 assert_eq!(usdt_balance.currency, Currency::USDT());
647 assert_eq!(usdt_balance.total.as_f64(), 10000.0);
648 assert_eq!(usdt_balance.free.as_f64(), 9500.0);
649 assert_eq!(usdt_balance.locked.as_f64(), 500.0);
650
651 assert_eq!(account_state.margins.len(), 0); }
653
654 #[rstest]
655 fn test_parse_margin_message_with_missing_fields() {
656 let margin_msg = BitmexMarginMsg {
658 account: 123456,
659 currency: Ustr::from("XBt"),
660 risk_limit: None,
661 amount: None,
662 prev_realised_pnl: None,
663 gross_comm: None,
664 gross_open_cost: None,
665 gross_open_premium: None,
666 gross_exec_cost: None,
667 gross_mark_value: None,
668 risk_value: None,
669 init_margin: None, maint_margin: None, target_excess_margin: None,
672 realised_pnl: None,
673 unrealised_pnl: None,
674 wallet_balance: Some(100000),
675 margin_balance: None,
676 margin_leverage: None,
677 margin_used_pcnt: None,
678 excess_margin: None,
679 available_margin: Some(95000),
680 withdrawable_margin: None,
681 maker_fee_discount: None,
682 taker_fee_discount: None,
683 timestamp: chrono::Utc::now(),
684 foreign_margin_balance: None,
685 foreign_requirement: None,
686 };
687
688 let account_id = AccountId::new("BITMEX-123456");
689 let ts_init = UnixNanos::from(1_000_000_000);
690
691 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
692 .expect("Should parse even with missing margin fields");
693
694 assert_eq!(account_state.balances.len(), 1);
696 assert_eq!(account_state.margins.len(), 0); }
698
699 #[rstest]
700 fn test_parse_margin_message_with_only_available_margin() {
701 let margin_msg = BitmexMarginMsg {
703 account: 1667725,
704 currency: Ustr::from("USDt"),
705 risk_limit: None,
706 amount: None,
707 prev_realised_pnl: None,
708 gross_comm: None,
709 gross_open_cost: None,
710 gross_open_premium: None,
711 gross_exec_cost: None,
712 gross_mark_value: None,
713 risk_value: None,
714 init_margin: None,
715 maint_margin: None,
716 target_excess_margin: None,
717 realised_pnl: None,
718 unrealised_pnl: None,
719 wallet_balance: None, margin_balance: None, margin_leverage: None,
722 margin_used_pcnt: None,
723 excess_margin: None,
724 available_margin: Some(107859036), withdrawable_margin: None,
726 maker_fee_discount: None,
727 taker_fee_discount: None,
728 timestamp: chrono::Utc::now(),
729 foreign_margin_balance: None,
730 foreign_requirement: None,
731 };
732
733 let account_id = AccountId::new("BITMEX-1667725");
734 let ts_init = UnixNanos::from(1_000_000_000);
735
736 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
737 .expect("Should handle case with only available_margin");
738
739 let balance = &account_state.balances[0];
741 assert_eq!(balance.currency, Currency::USDT());
742 assert_eq!(balance.total.as_f64(), 107.859036); assert_eq!(balance.free.as_f64(), 107.859036);
744 assert_eq!(balance.locked.as_f64(), 0.0);
745
746 assert_eq!(balance.total, balance.locked + balance.free);
748 }
749
750 #[rstest]
751 fn test_parse_margin_available_exceeds_wallet() {
752 let margin_msg = BitmexMarginMsg {
754 account: 123456,
755 currency: Ustr::from("XBt"),
756 risk_limit: None,
757 amount: Some(70772),
758 prev_realised_pnl: None,
759 gross_comm: None,
760 gross_open_cost: None,
761 gross_open_premium: None,
762 gross_exec_cost: None,
763 gross_mark_value: None,
764 risk_value: None,
765 init_margin: Some(0),
766 maint_margin: Some(0),
767 target_excess_margin: None,
768 realised_pnl: None,
769 unrealised_pnl: None,
770 wallet_balance: Some(70772), margin_balance: None,
772 margin_leverage: None,
773 margin_used_pcnt: None,
774 excess_margin: None,
775 available_margin: Some(94381), withdrawable_margin: None,
777 maker_fee_discount: None,
778 taker_fee_discount: None,
779 timestamp: chrono::Utc::now(),
780 foreign_margin_balance: None,
781 foreign_requirement: None,
782 };
783
784 let account_id = AccountId::new("BITMEX-123456");
785 let ts_init = UnixNanos::from(1_000_000_000);
786
787 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
788 .expect("Should handle available > wallet case");
789
790 let balance = &account_state.balances[0];
792 assert_eq!(balance.currency, Currency::from("XBT"));
793 assert_eq!(balance.total.as_f64(), 0.00070772); assert_eq!(balance.free.as_f64(), 0.00070772); assert_eq!(balance.locked.as_f64(), 0.0);
796
797 assert_eq!(balance.total, balance.locked + balance.free);
799 }
800
801 #[rstest]
802 fn test_parse_margin_message_with_foreign_requirements() {
803 let margin_msg = BitmexMarginMsg {
805 account: 123456,
806 currency: Ustr::from("XBt"),
807 risk_limit: Some(1000000000),
808 amount: Some(100000000), prev_realised_pnl: None,
810 gross_comm: None,
811 gross_open_cost: None,
812 gross_open_premium: None,
813 gross_exec_cost: None,
814 gross_mark_value: None,
815 risk_value: None,
816 init_margin: None, maint_margin: None, target_excess_margin: None,
819 realised_pnl: None,
820 unrealised_pnl: None,
821 wallet_balance: Some(100000000),
822 margin_balance: Some(100000000),
823 margin_leverage: None,
824 margin_used_pcnt: None,
825 excess_margin: None,
826 available_margin: Some(95000000), withdrawable_margin: None,
828 maker_fee_discount: None,
829 taker_fee_discount: None,
830 timestamp: chrono::Utc::now(),
831 foreign_margin_balance: Some(100000000), foreign_requirement: Some(5000000), };
834
835 let account_id = AccountId::new("BITMEX-123456");
836 let ts_init = UnixNanos::from(1_000_000_000);
837
838 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
839 .expect("Failed to parse account state with foreign requirements");
840
841 let balance = &account_state.balances[0];
843 assert_eq!(balance.currency, Currency::from("XBT"));
844 assert_eq!(balance.total.as_f64(), 1.0);
845 assert_eq!(balance.free.as_f64(), 0.95);
846 assert_eq!(balance.locked.as_f64(), 0.05);
847
848 assert_eq!(account_state.margins.len(), 0);
850 }
851
852 #[rstest]
853 fn test_parse_margin_message_with_both_standard_and_foreign() {
854 let margin_msg = BitmexMarginMsg {
856 account: 123456,
857 currency: Ustr::from("XBt"),
858 risk_limit: Some(1000000000),
859 amount: Some(100000000), prev_realised_pnl: None,
861 gross_comm: None,
862 gross_open_cost: None,
863 gross_open_premium: None,
864 gross_exec_cost: None,
865 gross_mark_value: None,
866 risk_value: None,
867 init_margin: Some(2000000), maint_margin: Some(1000000), target_excess_margin: None,
870 realised_pnl: None,
871 unrealised_pnl: None,
872 wallet_balance: Some(100000000),
873 margin_balance: Some(100000000),
874 margin_leverage: None,
875 margin_used_pcnt: None,
876 excess_margin: None,
877 available_margin: Some(93000000), withdrawable_margin: None,
879 maker_fee_discount: None,
880 taker_fee_discount: None,
881 timestamp: chrono::Utc::now(),
882 foreign_margin_balance: Some(100000000),
883 foreign_requirement: Some(5000000), };
885
886 let account_id = AccountId::new("BITMEX-123456");
887 let ts_init = UnixNanos::from(1_000_000_000);
888
889 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
890 .expect("Failed to parse account state with both margins");
891
892 let balance = &account_state.balances[0];
894 assert_eq!(balance.currency, Currency::from("XBT"));
895 assert_eq!(balance.total.as_f64(), 1.0);
896 assert_eq!(balance.free.as_f64(), 0.93);
897 assert_eq!(balance.locked.as_f64(), 0.07); assert_eq!(account_state.margins.len(), 0);
901 }
902}