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