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 log::warn!("Quantity {value} exceeds u32::MAX without instrument increment, clamping",);
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 log::warn!(
87 "Quantity {} converts to {units} contracts which exceeds u32::MAX, clamping",
88 quantity.as_f64(),
89 );
90 u32::MAX
91 }
92 None => {
93 log::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 log::warn!("Quantity value {value} exceeds QUANTITY_RAW_MAX {QUANTITY_RAW_MAX}, clamping",);
114 raw = QUANTITY_RAW_MAX;
115 }
116
117 Quantity::from_raw(raw, precision)
118}
119
120pub fn derive_contract_decimal_and_increment(
130 multiplier: Option<f64>,
131 max_scale: u32,
132) -> anyhow::Result<(Decimal, Quantity)> {
133 let raw_multiplier = multiplier.unwrap_or(1.0);
134 let contract_size = if raw_multiplier > 0.0 {
135 1.0 / raw_multiplier
136 } else {
137 1.0
138 };
139
140 let mut contract_decimal = Decimal::from_f64_retain(contract_size)
141 .ok_or_else(|| anyhow::anyhow!("Invalid contract size {contract_size}"))?;
142 if contract_decimal.scale() > max_scale {
143 contract_decimal = contract_decimal
144 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
145 }
146 contract_decimal = contract_decimal.normalize();
147 let contract_precision = contract_decimal.scale() as u8;
148 let size_increment = Quantity::from_decimal_dp(contract_decimal, contract_precision)?;
149
150 Ok((contract_decimal, size_increment))
151}
152
153pub fn convert_contract_quantity(
160 value: Option<f64>,
161 contract_decimal: Decimal,
162 max_scale: u32,
163 field_name: &str,
164) -> anyhow::Result<Option<Quantity>> {
165 value
166 .map(|raw| {
167 let mut decimal = Decimal::from_f64_retain(raw)
168 .ok_or_else(|| anyhow::anyhow!("Invalid {field_name} value"))?
169 * contract_decimal;
170 let scale = decimal.scale();
171 if scale > max_scale {
172 decimal = decimal
173 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
174 }
175 let decimal = decimal.normalize();
176 let precision = decimal.scale() as u8;
177 Quantity::from_decimal_dp(decimal, precision)
178 })
179 .transpose()
180}
181
182#[must_use]
184pub fn parse_signed_contracts_quantity(value: i64, instrument: &InstrumentAny) -> Quantity {
185 let abs_value = value.checked_abs().unwrap_or_else(|| {
186 log::warn!("Quantity value {value} overflowed when taking absolute value");
187 i64::MAX
188 }) as u64;
189 parse_contracts_quantity(abs_value, instrument)
190}
191
192#[must_use]
194pub fn parse_fractional_quantity(value: f64, instrument: &InstrumentAny) -> Quantity {
195 if value < 0.0 {
196 log::warn!("Received negative fractional quantity {value}, defaulting to 0.0");
197 return instrument.make_qty(0.0, None);
198 }
199
200 instrument.try_make_qty(value, None).unwrap_or_else(|err| {
201 log::warn!(
202 "Failed to convert fractional quantity {value} with precision {}: {err}",
203 instrument.size_precision(),
204 );
205 instrument.make_qty(0.0, None)
206 })
207}
208
209#[must_use]
217pub fn normalize_trade_bin_prices(
218 open: Price,
219 mut high: Price,
220 mut low: Price,
221 close: Price,
222 symbol: &Ustr,
223 bar_type: Option<&BarType>,
224) -> (Price, Price, Price, Price) {
225 let price_extremes = [open, high, low, close];
226 let max_price = *price_extremes
227 .iter()
228 .max()
229 .expect("Price array contains values");
230 let min_price = *price_extremes
231 .iter()
232 .min()
233 .expect("Price array contains values");
234
235 if high < max_price || low > min_price {
236 match bar_type {
237 Some(bt) => {
238 log::warn!("Adjusting BitMEX trade bin extremes: symbol={symbol}, bar_type={bt:?}");
239 }
240 None => log::warn!("Adjusting BitMEX trade bin extremes: symbol={symbol}"),
241 }
242 high = max_price;
243 low = min_price;
244 }
245
246 (open, high, low, close)
247}
248
249#[must_use]
252pub fn normalize_trade_bin_volume(volume: Option<i64>, symbol: &Ustr) -> u64 {
253 match volume {
254 Some(v) if v >= 0 => v as u64,
255 Some(v) => {
256 log::warn!("Received negative volume in BitMEX trade bin: symbol={symbol}, volume={v}");
257 0
258 }
259 None => {
260 log::warn!("Trade bin missing volume, defaulting to 0: symbol={symbol}");
261 0
262 }
263 }
264}
265
266#[must_use]
271pub fn parse_optional_datetime_to_unix_nanos(
272 value: &Option<DateTime<Utc>>,
273 field: &str,
274) -> UnixNanos {
275 value
276 .map(|dt| {
277 UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or_else(|| {
278 log::error!("Invalid timestamp - out of range: field={field}, timestamp={dt:?}");
279 0
280 }) as u64)
281 })
282 .unwrap_or_default()
283}
284
285#[must_use]
287pub const fn parse_aggressor_side(side: &Option<BitmexSide>) -> AggressorSide {
288 match side {
289 Some(BitmexSide::Buy) => AggressorSide::Buyer,
290 Some(BitmexSide::Sell) => AggressorSide::Seller,
291 None => AggressorSide::NoAggressor,
292 }
293}
294
295#[must_use]
297pub fn parse_liquidity_side(liquidity: &Option<BitmexLiquidityIndicator>) -> LiquiditySide {
298 liquidity
299 .map(std::convert::Into::into)
300 .unwrap_or(LiquiditySide::NoLiquiditySide)
301}
302
303#[must_use]
305pub const fn parse_position_side(current_qty: Option<i64>) -> PositionSide {
306 match current_qty {
307 Some(qty) if qty > 0 => PositionSide::Long,
308 Some(qty) if qty < 0 => PositionSide::Short,
309 _ => PositionSide::Flat,
310 }
311}
312
313#[must_use]
324pub fn map_bitmex_currency(bitmex_currency: &str) -> Cow<'static, str> {
325 match bitmex_currency {
326 "XBt" => Cow::Borrowed("XBT"),
327 "USDt" | "LAMp" => Cow::Borrowed("USDT"), "RLUSd" => Cow::Borrowed("RLUSD"),
329 "MAMUSd" => Cow::Borrowed("MAMUSD"),
330 other => Cow::Owned(other.to_uppercase()),
331 }
332}
333
334pub fn parse_account_state(
340 margin: &BitmexMarginMsg,
341 account_id: AccountId,
342 ts_init: UnixNanos,
343) -> anyhow::Result<AccountState> {
344 log::debug!(
345 "Parsing margin: currency={}, wallet_balance={:?}, available_margin={:?}, init_margin={:?}, maint_margin={:?}, foreign_margin_balance={:?}, foreign_requirement={:?}",
346 margin.currency,
347 margin.wallet_balance,
348 margin.available_margin,
349 margin.init_margin,
350 margin.maint_margin,
351 margin.foreign_margin_balance,
352 margin.foreign_requirement
353 );
354
355 let currency_str = map_bitmex_currency(&margin.currency);
356
357 let currency = match Currency::try_from_str(¤cy_str) {
358 Some(c) => c,
359 None => {
360 log::warn!(
362 "Unknown currency '{currency_str}' in margin message, creating default crypto currency"
363 );
364 let currency = Currency::new(¤cy_str, 8, 0, ¤cy_str, CurrencyType::Crypto);
365 if let Err(e) = Currency::register(currency, false) {
366 log::error!("Failed to register currency '{currency_str}': {e}");
367 }
368 currency
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)]
452mod tests {
453 use chrono::TimeZone;
454 use nautilus_model::{instruments::CurrencyPair, types::fixed::FIXED_PRECISION};
455 use rstest::rstest;
456 use ustr::Ustr;
457
458 use super::*;
459
460 #[rstest]
461 fn test_clean_reason_strips_nautilus_trader() {
462 assert_eq!(
463 clean_reason(
464 "Canceled: Order had execInst of ParticipateDoNotInitiate\nNautilusTrader"
465 ),
466 "Canceled: Order had execInst of ParticipateDoNotInitiate"
467 );
468
469 assert_eq!(clean_reason("Some error\nNautilusTrader"), "Some error");
470 assert_eq!(
471 clean_reason("Multiple lines\nSome content\nNautilusTrader"),
472 "Multiple lines\nSome content"
473 );
474 assert_eq!(clean_reason("No identifier here"), "No identifier here");
475 assert_eq!(clean_reason(" \nNautilusTrader "), "");
476 }
477
478 fn make_test_spot_instrument(size_increment: f64, size_precision: u8) -> InstrumentAny {
479 let instrument_id = InstrumentId::from("SOLUSDT.BITMEX");
480 let raw_symbol = Symbol::from("SOLUSDT");
481 let base_currency = Currency::from("SOL");
482 let quote_currency = Currency::from("USDT");
483 let price_precision = 2;
484 let price_increment = Price::new(0.01, price_precision);
485 let size_increment = Quantity::new(size_increment, size_precision);
486 let instrument = CurrencyPair::new(
487 instrument_id,
488 raw_symbol,
489 base_currency,
490 quote_currency,
491 price_precision,
492 size_precision,
493 price_increment,
494 size_increment,
495 None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::from(0),
508 UnixNanos::from(0),
509 );
510 InstrumentAny::CurrencyPair(instrument)
511 }
512
513 #[rstest]
514 fn test_quantity_to_u32_scaled() {
515 let instrument = make_test_spot_instrument(0.0001, 4);
516 let qty = Quantity::new(0.1, 4);
517 assert_eq!(quantity_to_u32(&qty, &instrument), 1_000);
518 }
519
520 #[rstest]
521 fn test_parse_contracts_quantity_scaled() {
522 let instrument = make_test_spot_instrument(0.0001, 4);
523 let qty = parse_contracts_quantity(1_000, &instrument);
524 assert!((qty.as_f64() - 0.1).abs() < 1e-9);
525 assert_eq!(qty.precision, 4);
526 }
527
528 #[rstest]
529 fn test_convert_contract_quantity_scaling() {
530 let max_scale = FIXED_PRECISION as u32;
531 let (contract_decimal, size_increment) =
532 derive_contract_decimal_and_increment(Some(10_000.0), max_scale).unwrap();
533 assert!((size_increment.as_f64() - 0.0001).abs() < 1e-12);
534
535 let lot_qty =
536 convert_contract_quantity(Some(1_000.0), contract_decimal, max_scale, "lot size")
537 .unwrap()
538 .unwrap();
539 assert!((lot_qty.as_f64() - 0.1).abs() < 1e-9);
540 assert_eq!(lot_qty.precision, 1);
541 }
542
543 #[rstest]
544 fn test_derive_contract_decimal_defaults_to_one() {
545 let max_scale = FIXED_PRECISION as u32;
546 let (contract_decimal, size_increment) =
547 derive_contract_decimal_and_increment(Some(0.0), max_scale).unwrap();
548 assert_eq!(contract_decimal, Decimal::ONE);
549 assert_eq!(size_increment.as_f64(), 1.0);
550 }
551
552 #[rstest]
553 fn test_parse_account_state() {
554 let margin_msg = BitmexMarginMsg {
555 account: 123456,
556 currency: Ustr::from("XBt"),
557 risk_limit: Some(1000000000),
558 amount: Some(5000000),
559 prev_realised_pnl: Some(100000),
560 gross_comm: Some(1000),
561 gross_open_cost: Some(200000),
562 gross_open_premium: None,
563 gross_exec_cost: None,
564 gross_mark_value: Some(210000),
565 risk_value: Some(50000),
566 init_margin: Some(20000),
567 maint_margin: Some(10000),
568 target_excess_margin: Some(5000),
569 realised_pnl: Some(100000),
570 unrealised_pnl: Some(10000),
571 wallet_balance: Some(5000000),
572 margin_balance: Some(5010000),
573 margin_leverage: Some(2.5),
574 margin_used_pcnt: Some(0.25),
575 excess_margin: Some(4990000),
576 available_margin: Some(4980000),
577 withdrawable_margin: Some(4900000),
578 maker_fee_discount: Some(0.1),
579 taker_fee_discount: Some(0.05),
580 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
581 foreign_margin_balance: None,
582 foreign_requirement: None,
583 };
584
585 let account_id = AccountId::new("BITMEX-001");
586 let ts_init = UnixNanos::from(1_000_000_000);
587
588 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
589
590 assert_eq!(account_state.account_id, account_id);
591 assert_eq!(account_state.account_type, AccountType::Margin);
592 assert_eq!(account_state.balances.len(), 1);
593 assert_eq!(account_state.margins.len(), 0); assert!(account_state.is_reported);
595
596 let xbt_balance = &account_state.balances[0];
597 assert_eq!(xbt_balance.currency, Currency::from("XBT"));
598 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); }
602
603 #[rstest]
604 fn test_parse_account_state_usdt() {
605 let margin_msg = BitmexMarginMsg {
606 account: 123456,
607 currency: Ustr::from("USDt"),
608 risk_limit: Some(1000000000),
609 amount: Some(10000000000), prev_realised_pnl: None,
611 gross_comm: None,
612 gross_open_cost: None,
613 gross_open_premium: None,
614 gross_exec_cost: None,
615 gross_mark_value: None,
616 risk_value: None,
617 init_margin: Some(500000), maint_margin: Some(250000), target_excess_margin: None,
620 realised_pnl: None,
621 unrealised_pnl: None,
622 wallet_balance: Some(10000000000),
623 margin_balance: Some(10000000000),
624 margin_leverage: None,
625 margin_used_pcnt: None,
626 excess_margin: None,
627 available_margin: Some(9500000000), withdrawable_margin: None,
629 maker_fee_discount: None,
630 taker_fee_discount: None,
631 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
632 foreign_margin_balance: None,
633 foreign_requirement: None,
634 };
635
636 let account_id = AccountId::new("BITMEX-001");
637 let ts_init = UnixNanos::from(1_000_000_000);
638
639 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
640
641 let usdt_balance = &account_state.balances[0];
642 assert_eq!(usdt_balance.currency, Currency::USDT());
643 assert_eq!(usdt_balance.total.as_f64(), 10000.0);
644 assert_eq!(usdt_balance.free.as_f64(), 9500.0);
645 assert_eq!(usdt_balance.locked.as_f64(), 500.0);
646
647 assert_eq!(account_state.margins.len(), 0); }
649
650 #[rstest]
651 fn test_parse_margin_message_with_missing_fields() {
652 let margin_msg = BitmexMarginMsg {
654 account: 123456,
655 currency: Ustr::from("XBt"),
656 risk_limit: None,
657 amount: None,
658 prev_realised_pnl: None,
659 gross_comm: None,
660 gross_open_cost: None,
661 gross_open_premium: None,
662 gross_exec_cost: None,
663 gross_mark_value: None,
664 risk_value: None,
665 init_margin: None, maint_margin: None, target_excess_margin: None,
668 realised_pnl: None,
669 unrealised_pnl: None,
670 wallet_balance: Some(100000),
671 margin_balance: None,
672 margin_leverage: None,
673 margin_used_pcnt: None,
674 excess_margin: None,
675 available_margin: Some(95000),
676 withdrawable_margin: None,
677 maker_fee_discount: None,
678 taker_fee_discount: None,
679 timestamp: chrono::Utc::now(),
680 foreign_margin_balance: None,
681 foreign_requirement: None,
682 };
683
684 let account_id = AccountId::new("BITMEX-123456");
685 let ts_init = UnixNanos::from(1_000_000_000);
686
687 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
688 .expect("Should parse even with missing margin fields");
689
690 assert_eq!(account_state.balances.len(), 1);
692 assert_eq!(account_state.margins.len(), 0); }
694
695 #[rstest]
696 fn test_parse_margin_message_with_only_available_margin() {
697 let margin_msg = BitmexMarginMsg {
699 account: 1667725,
700 currency: Ustr::from("USDt"),
701 risk_limit: None,
702 amount: None,
703 prev_realised_pnl: None,
704 gross_comm: None,
705 gross_open_cost: None,
706 gross_open_premium: None,
707 gross_exec_cost: None,
708 gross_mark_value: None,
709 risk_value: None,
710 init_margin: None,
711 maint_margin: None,
712 target_excess_margin: None,
713 realised_pnl: None,
714 unrealised_pnl: None,
715 wallet_balance: None, margin_balance: None, margin_leverage: None,
718 margin_used_pcnt: None,
719 excess_margin: None,
720 available_margin: Some(107859036), withdrawable_margin: None,
722 maker_fee_discount: None,
723 taker_fee_discount: None,
724 timestamp: chrono::Utc::now(),
725 foreign_margin_balance: None,
726 foreign_requirement: None,
727 };
728
729 let account_id = AccountId::new("BITMEX-1667725");
730 let ts_init = UnixNanos::from(1_000_000_000);
731
732 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
733 .expect("Should handle case with only available_margin");
734
735 let balance = &account_state.balances[0];
737 assert_eq!(balance.currency, Currency::USDT());
738 assert_eq!(balance.total.as_f64(), 107.859036); assert_eq!(balance.free.as_f64(), 107.859036);
740 assert_eq!(balance.locked.as_f64(), 0.0);
741
742 assert_eq!(balance.total, balance.locked + balance.free);
744 }
745
746 #[rstest]
747 fn test_parse_margin_available_exceeds_wallet() {
748 let margin_msg = BitmexMarginMsg {
750 account: 123456,
751 currency: Ustr::from("XBt"),
752 risk_limit: None,
753 amount: Some(70772),
754 prev_realised_pnl: None,
755 gross_comm: None,
756 gross_open_cost: None,
757 gross_open_premium: None,
758 gross_exec_cost: None,
759 gross_mark_value: None,
760 risk_value: None,
761 init_margin: Some(0),
762 maint_margin: Some(0),
763 target_excess_margin: None,
764 realised_pnl: None,
765 unrealised_pnl: None,
766 wallet_balance: Some(70772), margin_balance: None,
768 margin_leverage: None,
769 margin_used_pcnt: None,
770 excess_margin: None,
771 available_margin: Some(94381), withdrawable_margin: None,
773 maker_fee_discount: None,
774 taker_fee_discount: None,
775 timestamp: chrono::Utc::now(),
776 foreign_margin_balance: None,
777 foreign_requirement: None,
778 };
779
780 let account_id = AccountId::new("BITMEX-123456");
781 let ts_init = UnixNanos::from(1_000_000_000);
782
783 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
784 .expect("Should handle available > wallet case");
785
786 let balance = &account_state.balances[0];
788 assert_eq!(balance.currency, Currency::from("XBT"));
789 assert_eq!(balance.total.as_f64(), 0.00070772); assert_eq!(balance.free.as_f64(), 0.00070772); assert_eq!(balance.locked.as_f64(), 0.0);
792
793 assert_eq!(balance.total, balance.locked + balance.free);
795 }
796
797 #[rstest]
798 fn test_parse_margin_message_with_foreign_requirements() {
799 let margin_msg = BitmexMarginMsg {
801 account: 123456,
802 currency: Ustr::from("XBt"),
803 risk_limit: Some(1000000000),
804 amount: Some(100000000), prev_realised_pnl: None,
806 gross_comm: None,
807 gross_open_cost: None,
808 gross_open_premium: None,
809 gross_exec_cost: None,
810 gross_mark_value: None,
811 risk_value: None,
812 init_margin: None, maint_margin: None, target_excess_margin: None,
815 realised_pnl: None,
816 unrealised_pnl: None,
817 wallet_balance: Some(100000000),
818 margin_balance: Some(100000000),
819 margin_leverage: None,
820 margin_used_pcnt: None,
821 excess_margin: None,
822 available_margin: Some(95000000), withdrawable_margin: None,
824 maker_fee_discount: None,
825 taker_fee_discount: None,
826 timestamp: chrono::Utc::now(),
827 foreign_margin_balance: Some(100000000), foreign_requirement: Some(5000000), };
830
831 let account_id = AccountId::new("BITMEX-123456");
832 let ts_init = UnixNanos::from(1_000_000_000);
833
834 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
835 .expect("Failed to parse account state with foreign requirements");
836
837 let balance = &account_state.balances[0];
839 assert_eq!(balance.currency, Currency::from("XBT"));
840 assert_eq!(balance.total.as_f64(), 1.0);
841 assert_eq!(balance.free.as_f64(), 0.95);
842 assert_eq!(balance.locked.as_f64(), 0.05);
843
844 assert_eq!(account_state.margins.len(), 0);
846 }
847
848 #[rstest]
849 fn test_parse_margin_message_with_both_standard_and_foreign() {
850 let margin_msg = BitmexMarginMsg {
852 account: 123456,
853 currency: Ustr::from("XBt"),
854 risk_limit: Some(1000000000),
855 amount: Some(100000000), prev_realised_pnl: None,
857 gross_comm: None,
858 gross_open_cost: None,
859 gross_open_premium: None,
860 gross_exec_cost: None,
861 gross_mark_value: None,
862 risk_value: None,
863 init_margin: Some(2000000), maint_margin: Some(1000000), target_excess_margin: None,
866 realised_pnl: None,
867 unrealised_pnl: None,
868 wallet_balance: Some(100000000),
869 margin_balance: Some(100000000),
870 margin_leverage: None,
871 margin_used_pcnt: None,
872 excess_margin: None,
873 available_margin: Some(93000000), withdrawable_margin: None,
875 maker_fee_discount: None,
876 taker_fee_discount: None,
877 timestamp: chrono::Utc::now(),
878 foreign_margin_balance: Some(100000000),
879 foreign_requirement: Some(5000000), };
881
882 let account_id = AccountId::new("BITMEX-123456");
883 let ts_init = UnixNanos::from(1_000_000_000);
884
885 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
886 .expect("Failed to parse account state with both margins");
887
888 let balance = &account_state.balances[0];
890 assert_eq!(balance.currency, Currency::from("XBT"));
891 assert_eq!(balance.total.as_f64(), 1.0);
892 assert_eq!(balance.free.as_f64(), 0.93);
893 assert_eq!(balance.locked.as_f64(), 0.07); assert_eq!(account_state.margins.len(), 0);
897 }
898}