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_dp(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_dp(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 let currency = Currency::new(¤cy_str, 8, 0, ¤cy_str, CurrencyType::Crypto);
369 if let Err(e) = Currency::register(currency, false) {
370 tracing::error!("Failed to register currency '{currency_str}': {e}");
371 }
372 currency
373 }
374 };
375
376 let divisor = if margin.currency == "XBt" {
378 100_000_000.0 } else if margin.currency == "USDt" || margin.currency == "LAMp" {
380 1_000_000.0 } else {
382 1.0
383 };
384
385 let total = if let Some(wallet_balance) = margin.wallet_balance {
387 Money::new(wallet_balance as f64 / divisor, currency)
388 } else if let Some(margin_balance) = margin.margin_balance {
389 Money::new(margin_balance as f64 / divisor, currency)
390 } else if let Some(available) = margin.available_margin {
391 Money::new(available as f64 / divisor, currency)
393 } else {
394 Money::new(0.0, currency)
395 };
396
397 let margin_used = if let Some(init_margin) = margin.init_margin {
399 Money::new(init_margin as f64 / divisor, currency)
400 } else {
401 Money::new(0.0, currency)
402 };
403
404 let free = if let Some(withdrawable) = margin.withdrawable_margin {
406 Money::new(withdrawable as f64 / divisor, currency)
407 } else if let Some(available) = margin.available_margin {
408 let available_money = Money::new(available as f64 / divisor, currency);
410 if available_money > total {
412 total
413 } else {
414 available_money
415 }
416 } else {
417 let calculated_free = total - margin_used;
419 if calculated_free < Money::new(0.0, currency) {
420 Money::new(0.0, currency)
421 } else {
422 calculated_free
423 }
424 };
425
426 let locked = total - free;
428
429 let balance = AccountBalance::new(total, locked, free);
430 let balances = vec![balance];
431
432 let margins = Vec::new();
435
436 let account_type = AccountType::Margin;
437 let is_reported = true;
438 let event_id = UUID4::new();
439 let ts_event =
440 UnixNanos::from(margin.timestamp.timestamp_nanos_opt().unwrap_or_default() as u64);
441
442 Ok(AccountState::new(
443 account_id,
444 account_type,
445 balances,
446 margins,
447 is_reported,
448 event_id,
449 ts_event,
450 ts_init,
451 None,
452 ))
453}
454
455#[cfg(test)]
460mod tests {
461 use chrono::TimeZone;
462 use nautilus_model::{instruments::CurrencyPair, types::fixed::FIXED_PRECISION};
463 use rstest::rstest;
464 use ustr::Ustr;
465
466 use super::*;
467
468 #[rstest]
469 fn test_clean_reason_strips_nautilus_trader() {
470 assert_eq!(
471 clean_reason(
472 "Canceled: Order had execInst of ParticipateDoNotInitiate\nNautilusTrader"
473 ),
474 "Canceled: Order had execInst of ParticipateDoNotInitiate"
475 );
476
477 assert_eq!(clean_reason("Some error\nNautilusTrader"), "Some error");
478 assert_eq!(
479 clean_reason("Multiple lines\nSome content\nNautilusTrader"),
480 "Multiple lines\nSome content"
481 );
482 assert_eq!(clean_reason("No identifier here"), "No identifier here");
483 assert_eq!(clean_reason(" \nNautilusTrader "), "");
484 }
485
486 fn make_test_spot_instrument(size_increment: f64, size_precision: u8) -> InstrumentAny {
487 let instrument_id = InstrumentId::from("SOLUSDT.BITMEX");
488 let raw_symbol = Symbol::from("SOLUSDT");
489 let base_currency = Currency::from("SOL");
490 let quote_currency = Currency::from("USDT");
491 let price_precision = 2;
492 let price_increment = Price::new(0.01, price_precision);
493 let size_increment = Quantity::new(size_increment, size_precision);
494 let instrument = CurrencyPair::new(
495 instrument_id,
496 raw_symbol,
497 base_currency,
498 quote_currency,
499 price_precision,
500 size_precision,
501 price_increment,
502 size_increment,
503 None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::from(0),
516 UnixNanos::from(0),
517 );
518 InstrumentAny::CurrencyPair(instrument)
519 }
520
521 #[rstest]
522 fn test_quantity_to_u32_scaled() {
523 let instrument = make_test_spot_instrument(0.0001, 4);
524 let qty = Quantity::new(0.1, 4);
525 assert_eq!(quantity_to_u32(&qty, &instrument), 1_000);
526 }
527
528 #[rstest]
529 fn test_parse_contracts_quantity_scaled() {
530 let instrument = make_test_spot_instrument(0.0001, 4);
531 let qty = parse_contracts_quantity(1_000, &instrument);
532 assert!((qty.as_f64() - 0.1).abs() < 1e-9);
533 assert_eq!(qty.precision, 4);
534 }
535
536 #[rstest]
537 fn test_convert_contract_quantity_scaling() {
538 let max_scale = FIXED_PRECISION as u32;
539 let (contract_decimal, size_increment) =
540 derive_contract_decimal_and_increment(Some(10_000.0), max_scale).unwrap();
541 assert!((size_increment.as_f64() - 0.0001).abs() < 1e-12);
542
543 let lot_qty =
544 convert_contract_quantity(Some(1_000.0), contract_decimal, max_scale, "lot size")
545 .unwrap()
546 .unwrap();
547 assert!((lot_qty.as_f64() - 0.1).abs() < 1e-9);
548 assert_eq!(lot_qty.precision, 1);
549 }
550
551 #[rstest]
552 fn test_derive_contract_decimal_defaults_to_one() {
553 let max_scale = FIXED_PRECISION as u32;
554 let (contract_decimal, size_increment) =
555 derive_contract_decimal_and_increment(Some(0.0), max_scale).unwrap();
556 assert_eq!(contract_decimal, Decimal::ONE);
557 assert_eq!(size_increment.as_f64(), 1.0);
558 }
559
560 #[rstest]
561 fn test_parse_account_state() {
562 let margin_msg = BitmexMarginMsg {
563 account: 123456,
564 currency: Ustr::from("XBt"),
565 risk_limit: Some(1000000000),
566 amount: Some(5000000),
567 prev_realised_pnl: Some(100000),
568 gross_comm: Some(1000),
569 gross_open_cost: Some(200000),
570 gross_open_premium: None,
571 gross_exec_cost: None,
572 gross_mark_value: Some(210000),
573 risk_value: Some(50000),
574 init_margin: Some(20000),
575 maint_margin: Some(10000),
576 target_excess_margin: Some(5000),
577 realised_pnl: Some(100000),
578 unrealised_pnl: Some(10000),
579 wallet_balance: Some(5000000),
580 margin_balance: Some(5010000),
581 margin_leverage: Some(2.5),
582 margin_used_pcnt: Some(0.25),
583 excess_margin: Some(4990000),
584 available_margin: Some(4980000),
585 withdrawable_margin: Some(4900000),
586 maker_fee_discount: Some(0.1),
587 taker_fee_discount: Some(0.05),
588 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
589 foreign_margin_balance: None,
590 foreign_requirement: None,
591 };
592
593 let account_id = AccountId::new("BITMEX-001");
594 let ts_init = UnixNanos::from(1_000_000_000);
595
596 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
597
598 assert_eq!(account_state.account_id, account_id);
599 assert_eq!(account_state.account_type, AccountType::Margin);
600 assert_eq!(account_state.balances.len(), 1);
601 assert_eq!(account_state.margins.len(), 0); assert!(account_state.is_reported);
603
604 let xbt_balance = &account_state.balances[0];
605 assert_eq!(xbt_balance.currency, Currency::from("XBT"));
606 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); }
610
611 #[rstest]
612 fn test_parse_account_state_usdt() {
613 let margin_msg = BitmexMarginMsg {
614 account: 123456,
615 currency: Ustr::from("USDt"),
616 risk_limit: Some(1000000000),
617 amount: Some(10000000000), prev_realised_pnl: None,
619 gross_comm: None,
620 gross_open_cost: None,
621 gross_open_premium: None,
622 gross_exec_cost: None,
623 gross_mark_value: None,
624 risk_value: None,
625 init_margin: Some(500000), maint_margin: Some(250000), target_excess_margin: None,
628 realised_pnl: None,
629 unrealised_pnl: None,
630 wallet_balance: Some(10000000000),
631 margin_balance: Some(10000000000),
632 margin_leverage: None,
633 margin_used_pcnt: None,
634 excess_margin: None,
635 available_margin: Some(9500000000), withdrawable_margin: None,
637 maker_fee_discount: None,
638 taker_fee_discount: None,
639 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
640 foreign_margin_balance: None,
641 foreign_requirement: None,
642 };
643
644 let account_id = AccountId::new("BITMEX-001");
645 let ts_init = UnixNanos::from(1_000_000_000);
646
647 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
648
649 let usdt_balance = &account_state.balances[0];
650 assert_eq!(usdt_balance.currency, Currency::USDT());
651 assert_eq!(usdt_balance.total.as_f64(), 10000.0);
652 assert_eq!(usdt_balance.free.as_f64(), 9500.0);
653 assert_eq!(usdt_balance.locked.as_f64(), 500.0);
654
655 assert_eq!(account_state.margins.len(), 0); }
657
658 #[rstest]
659 fn test_parse_margin_message_with_missing_fields() {
660 let margin_msg = BitmexMarginMsg {
662 account: 123456,
663 currency: Ustr::from("XBt"),
664 risk_limit: None,
665 amount: None,
666 prev_realised_pnl: None,
667 gross_comm: None,
668 gross_open_cost: None,
669 gross_open_premium: None,
670 gross_exec_cost: None,
671 gross_mark_value: None,
672 risk_value: None,
673 init_margin: None, maint_margin: None, target_excess_margin: None,
676 realised_pnl: None,
677 unrealised_pnl: None,
678 wallet_balance: Some(100000),
679 margin_balance: None,
680 margin_leverage: None,
681 margin_used_pcnt: None,
682 excess_margin: None,
683 available_margin: Some(95000),
684 withdrawable_margin: None,
685 maker_fee_discount: None,
686 taker_fee_discount: None,
687 timestamp: chrono::Utc::now(),
688 foreign_margin_balance: None,
689 foreign_requirement: None,
690 };
691
692 let account_id = AccountId::new("BITMEX-123456");
693 let ts_init = UnixNanos::from(1_000_000_000);
694
695 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
696 .expect("Should parse even with missing margin fields");
697
698 assert_eq!(account_state.balances.len(), 1);
700 assert_eq!(account_state.margins.len(), 0); }
702
703 #[rstest]
704 fn test_parse_margin_message_with_only_available_margin() {
705 let margin_msg = BitmexMarginMsg {
707 account: 1667725,
708 currency: Ustr::from("USDt"),
709 risk_limit: None,
710 amount: None,
711 prev_realised_pnl: None,
712 gross_comm: None,
713 gross_open_cost: None,
714 gross_open_premium: None,
715 gross_exec_cost: None,
716 gross_mark_value: None,
717 risk_value: None,
718 init_margin: None,
719 maint_margin: None,
720 target_excess_margin: None,
721 realised_pnl: None,
722 unrealised_pnl: None,
723 wallet_balance: None, margin_balance: None, margin_leverage: None,
726 margin_used_pcnt: None,
727 excess_margin: None,
728 available_margin: Some(107859036), withdrawable_margin: None,
730 maker_fee_discount: None,
731 taker_fee_discount: None,
732 timestamp: chrono::Utc::now(),
733 foreign_margin_balance: None,
734 foreign_requirement: None,
735 };
736
737 let account_id = AccountId::new("BITMEX-1667725");
738 let ts_init = UnixNanos::from(1_000_000_000);
739
740 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
741 .expect("Should handle case with only available_margin");
742
743 let balance = &account_state.balances[0];
745 assert_eq!(balance.currency, Currency::USDT());
746 assert_eq!(balance.total.as_f64(), 107.859036); assert_eq!(balance.free.as_f64(), 107.859036);
748 assert_eq!(balance.locked.as_f64(), 0.0);
749
750 assert_eq!(balance.total, balance.locked + balance.free);
752 }
753
754 #[rstest]
755 fn test_parse_margin_available_exceeds_wallet() {
756 let margin_msg = BitmexMarginMsg {
758 account: 123456,
759 currency: Ustr::from("XBt"),
760 risk_limit: None,
761 amount: Some(70772),
762 prev_realised_pnl: None,
763 gross_comm: None,
764 gross_open_cost: None,
765 gross_open_premium: None,
766 gross_exec_cost: None,
767 gross_mark_value: None,
768 risk_value: None,
769 init_margin: Some(0),
770 maint_margin: Some(0),
771 target_excess_margin: None,
772 realised_pnl: None,
773 unrealised_pnl: None,
774 wallet_balance: Some(70772), margin_balance: None,
776 margin_leverage: None,
777 margin_used_pcnt: None,
778 excess_margin: None,
779 available_margin: Some(94381), withdrawable_margin: None,
781 maker_fee_discount: None,
782 taker_fee_discount: None,
783 timestamp: chrono::Utc::now(),
784 foreign_margin_balance: None,
785 foreign_requirement: None,
786 };
787
788 let account_id = AccountId::new("BITMEX-123456");
789 let ts_init = UnixNanos::from(1_000_000_000);
790
791 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
792 .expect("Should handle available > wallet case");
793
794 let balance = &account_state.balances[0];
796 assert_eq!(balance.currency, Currency::from("XBT"));
797 assert_eq!(balance.total.as_f64(), 0.00070772); assert_eq!(balance.free.as_f64(), 0.00070772); assert_eq!(balance.locked.as_f64(), 0.0);
800
801 assert_eq!(balance.total, balance.locked + balance.free);
803 }
804
805 #[rstest]
806 fn test_parse_margin_message_with_foreign_requirements() {
807 let margin_msg = BitmexMarginMsg {
809 account: 123456,
810 currency: Ustr::from("XBt"),
811 risk_limit: Some(1000000000),
812 amount: Some(100000000), prev_realised_pnl: None,
814 gross_comm: None,
815 gross_open_cost: None,
816 gross_open_premium: None,
817 gross_exec_cost: None,
818 gross_mark_value: None,
819 risk_value: None,
820 init_margin: None, maint_margin: None, target_excess_margin: None,
823 realised_pnl: None,
824 unrealised_pnl: None,
825 wallet_balance: Some(100000000),
826 margin_balance: Some(100000000),
827 margin_leverage: None,
828 margin_used_pcnt: None,
829 excess_margin: None,
830 available_margin: Some(95000000), withdrawable_margin: None,
832 maker_fee_discount: None,
833 taker_fee_discount: None,
834 timestamp: chrono::Utc::now(),
835 foreign_margin_balance: Some(100000000), foreign_requirement: Some(5000000), };
838
839 let account_id = AccountId::new("BITMEX-123456");
840 let ts_init = UnixNanos::from(1_000_000_000);
841
842 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
843 .expect("Failed to parse account state with foreign requirements");
844
845 let balance = &account_state.balances[0];
847 assert_eq!(balance.currency, Currency::from("XBT"));
848 assert_eq!(balance.total.as_f64(), 1.0);
849 assert_eq!(balance.free.as_f64(), 0.95);
850 assert_eq!(balance.locked.as_f64(), 0.05);
851
852 assert_eq!(account_state.margins.len(), 0);
854 }
855
856 #[rstest]
857 fn test_parse_margin_message_with_both_standard_and_foreign() {
858 let margin_msg = BitmexMarginMsg {
860 account: 123456,
861 currency: Ustr::from("XBt"),
862 risk_limit: Some(1000000000),
863 amount: Some(100000000), prev_realised_pnl: None,
865 gross_comm: None,
866 gross_open_cost: None,
867 gross_open_premium: None,
868 gross_exec_cost: None,
869 gross_mark_value: None,
870 risk_value: None,
871 init_margin: Some(2000000), maint_margin: Some(1000000), target_excess_margin: None,
874 realised_pnl: None,
875 unrealised_pnl: None,
876 wallet_balance: Some(100000000),
877 margin_balance: Some(100000000),
878 margin_leverage: None,
879 margin_used_pcnt: None,
880 excess_margin: None,
881 available_margin: Some(93000000), withdrawable_margin: None,
883 maker_fee_discount: None,
884 taker_fee_discount: None,
885 timestamp: chrono::Utc::now(),
886 foreign_margin_balance: Some(100000000),
887 foreign_requirement: Some(5000000), };
889
890 let account_id = AccountId::new("BITMEX-123456");
891 let ts_init = UnixNanos::from(1_000_000_000);
892
893 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
894 .expect("Failed to parse account state with both margins");
895
896 let balance = &account_state.balances[0];
898 assert_eq!(balance.currency, Currency::from("XBT"));
899 assert_eq!(balance.total.as_f64(), 1.0);
900 assert_eq!(balance.free.as_f64(), 0.93);
901 assert_eq!(balance.locked.as_f64(), 0.07); assert_eq!(account_state.margins.len(), 0);
905 }
906}