1use std::{borrow::Cow, str::FromStr};
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, TriggerType},
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::{BitmexExecInstruction, 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 extract_trigger_type(exec_inst: Option<&Vec<BitmexExecInstruction>>) -> TriggerType {
55 if let Some(exec_insts) = exec_inst {
56 if exec_insts.contains(&BitmexExecInstruction::MarkPrice) {
57 TriggerType::MarkPrice
58 } else if exec_insts.contains(&BitmexExecInstruction::IndexPrice) {
59 TriggerType::IndexPrice
60 } else if exec_insts.contains(&BitmexExecInstruction::LastPrice) {
61 TriggerType::LastPrice
62 } else {
63 TriggerType::Default
64 }
65 } else {
66 TriggerType::Default
67 }
68}
69
70#[must_use]
72pub fn parse_instrument_id(symbol: Ustr) -> InstrumentId {
73 InstrumentId::new(Symbol::from_ustr_unchecked(symbol), *BITMEX_VENUE)
74}
75
76#[must_use]
84pub fn quantity_to_u32(quantity: &Quantity, instrument: &InstrumentAny) -> u32 {
85 let size_increment = instrument.size_increment();
86 let step_decimal = size_increment.as_decimal();
87
88 if step_decimal.is_zero() {
89 let value = quantity.as_f64();
90 if value > u32::MAX as f64 {
91 log::warn!("Quantity {value} exceeds u32::MAX without instrument increment, clamping",);
92 return u32::MAX;
93 }
94 return value.max(0.0) as u32;
95 }
96
97 let units_decimal = quantity.as_decimal() / step_decimal;
98 let rounded_units =
99 units_decimal.round_dp_with_strategy(0, RoundingStrategy::MidpointAwayFromZero);
100
101 match rounded_units.to_u128() {
102 Some(units) if units <= u32::MAX as u128 => units as u32,
103 Some(units) => {
104 log::warn!(
105 "Quantity {} converts to {units} contracts which exceeds u32::MAX, clamping",
106 quantity.as_f64(),
107 );
108 u32::MAX
109 }
110 None => {
111 log::warn!(
112 "Failed to convert quantity {} to venue units, defaulting to 0",
113 quantity.as_f64(),
114 );
115 0
116 }
117 }
118}
119
120#[must_use]
122pub fn parse_contracts_quantity(value: u64, instrument: &InstrumentAny) -> Quantity {
123 let size_increment = instrument.size_increment();
124 let precision = instrument.size_precision();
125
126 let increment_raw: QuantityRaw = (&size_increment).into();
127 let value_raw = QuantityRaw::from(value);
128
129 let mut raw = increment_raw.saturating_mul(value_raw);
130 if raw > QUANTITY_RAW_MAX {
131 log::warn!("Quantity value {value} exceeds QUANTITY_RAW_MAX {QUANTITY_RAW_MAX}, clamping",);
132 raw = QUANTITY_RAW_MAX;
133 }
134
135 Quantity::from_raw(raw, precision)
136}
137
138pub fn derive_contract_decimal_and_increment(
148 multiplier: Option<f64>,
149 max_scale: u32,
150) -> anyhow::Result<(Decimal, Quantity)> {
151 let raw_multiplier = multiplier.unwrap_or(1.0);
152 let contract_size = if raw_multiplier > 0.0 {
153 1.0 / raw_multiplier
154 } else {
155 1.0
156 };
157
158 let mut contract_decimal = Decimal::from_str(&contract_size.to_string())
159 .map_err(|_| anyhow::anyhow!("Invalid contract size {contract_size}"))?;
160 if contract_decimal.scale() > max_scale {
161 contract_decimal = contract_decimal
162 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
163 }
164 contract_decimal = contract_decimal.normalize();
165 let contract_precision = contract_decimal.scale() as u8;
166 let size_increment = Quantity::from_decimal_dp(contract_decimal, contract_precision)?;
167
168 Ok((contract_decimal, size_increment))
169}
170
171pub fn convert_contract_quantity(
178 value: Option<f64>,
179 contract_decimal: Decimal,
180 max_scale: u32,
181 field_name: &str,
182) -> anyhow::Result<Option<Quantity>> {
183 value
184 .map(|raw| {
185 let mut decimal = Decimal::from_str(&raw.to_string())
186 .map_err(|_| anyhow::anyhow!("Invalid {field_name} value"))?
187 * contract_decimal;
188 let scale = decimal.scale();
189 if scale > max_scale {
190 decimal = decimal
191 .round_dp_with_strategy(max_scale, RoundingStrategy::MidpointAwayFromZero);
192 }
193 let decimal = decimal.normalize();
194 let precision = decimal.scale() as u8;
195 Quantity::from_decimal_dp(decimal, precision)
196 })
197 .transpose()
198}
199
200#[must_use]
202pub fn parse_signed_contracts_quantity(value: i64, instrument: &InstrumentAny) -> Quantity {
203 let abs_value = value.checked_abs().unwrap_or_else(|| {
204 log::warn!("Quantity value {value} overflowed when taking absolute value");
205 i64::MAX
206 }) as u64;
207 parse_contracts_quantity(abs_value, instrument)
208}
209
210#[must_use]
212pub fn parse_fractional_quantity(value: f64, instrument: &InstrumentAny) -> Quantity {
213 if value < 0.0 {
214 log::warn!("Received negative fractional quantity {value}, defaulting to 0.0");
215 return instrument.make_qty(0.0, None);
216 }
217
218 instrument.try_make_qty(value, None).unwrap_or_else(|e| {
219 log::warn!(
220 "Failed to convert fractional quantity {value} with precision {}: {e}",
221 instrument.size_precision(),
222 );
223 instrument.make_qty(0.0, None)
224 })
225}
226
227#[must_use]
235pub fn normalize_trade_bin_prices(
236 open: Price,
237 mut high: Price,
238 mut low: Price,
239 close: Price,
240 symbol: &Ustr,
241 bar_type: Option<&BarType>,
242) -> (Price, Price, Price, Price) {
243 let price_extremes = [open, high, low, close];
244 let max_price = *price_extremes
245 .iter()
246 .max()
247 .expect("Price array contains values");
248 let min_price = *price_extremes
249 .iter()
250 .min()
251 .expect("Price array contains values");
252
253 if high < max_price || low > min_price {
254 match bar_type {
255 Some(bt) => {
256 log::warn!("Adjusting BitMEX trade bin extremes: symbol={symbol}, bar_type={bt:?}");
257 }
258 None => log::warn!("Adjusting BitMEX trade bin extremes: symbol={symbol}"),
259 }
260 high = max_price;
261 low = min_price;
262 }
263
264 (open, high, low, close)
265}
266
267#[must_use]
270pub fn normalize_trade_bin_volume(volume: Option<i64>, symbol: &Ustr) -> u64 {
271 match volume {
272 Some(v) if v >= 0 => v as u64,
273 Some(v) => {
274 log::warn!("Received negative volume in BitMEX trade bin: symbol={symbol}, volume={v}");
275 0
276 }
277 None => {
278 log::warn!("Trade bin missing volume, defaulting to 0: symbol={symbol}");
279 0
280 }
281 }
282}
283
284#[must_use]
289pub fn parse_optional_datetime_to_unix_nanos(
290 value: &Option<DateTime<Utc>>,
291 field: &str,
292) -> UnixNanos {
293 value
294 .map(|dt| {
295 UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or_else(|| {
296 log::error!("Invalid timestamp - out of range: field={field}, timestamp={dt:?}");
297 0
298 }) as u64)
299 })
300 .unwrap_or_default()
301}
302
303#[must_use]
305pub const fn parse_aggressor_side(side: &Option<BitmexSide>) -> AggressorSide {
306 match side {
307 Some(BitmexSide::Buy) => AggressorSide::Buyer,
308 Some(BitmexSide::Sell) => AggressorSide::Seller,
309 None => AggressorSide::NoAggressor,
310 }
311}
312
313#[must_use]
315pub fn parse_liquidity_side(liquidity: &Option<BitmexLiquidityIndicator>) -> LiquiditySide {
316 liquidity.map_or(LiquiditySide::NoLiquiditySide, std::convert::Into::into)
317}
318
319#[must_use]
321pub const fn parse_position_side(current_qty: Option<i64>) -> PositionSide {
322 match current_qty {
323 Some(qty) if qty > 0 => PositionSide::Long,
324 Some(qty) if qty < 0 => PositionSide::Short,
325 _ => PositionSide::Flat,
326 }
327}
328
329#[must_use]
340pub fn map_bitmex_currency(bitmex_currency: &str) -> Cow<'static, str> {
341 match bitmex_currency {
342 "XBt" => Cow::Borrowed("XBT"),
343 "USDt" | "LAMp" => Cow::Borrowed("USDT"), "RLUSd" => Cow::Borrowed("RLUSD"),
345 "MAMUSd" => Cow::Borrowed("MAMUSD"),
346 other => Cow::Owned(other.to_uppercase()),
347 }
348}
349
350pub fn parse_account_balance(margin: &BitmexMarginMsg) -> AccountBalance {
352 log::debug!(
353 "Parsing margin: currency={}, wallet_balance={:?}, available_margin={:?}, init_margin={:?}, maint_margin={:?}",
354 margin.currency,
355 margin.wallet_balance,
356 margin.available_margin,
357 margin.init_margin,
358 margin.maint_margin,
359 );
360
361 let currency_str = map_bitmex_currency(&margin.currency);
362
363 let currency = match Currency::try_from_str(¤cy_str) {
364 Some(c) => c,
365 None => {
366 log::warn!(
368 "Unknown currency '{currency_str}' in margin message, creating default crypto currency"
369 );
370 let currency = Currency::new(¤cy_str, 8, 0, ¤cy_str, CurrencyType::Crypto);
371 if let Err(e) = Currency::register(currency, false) {
372 log::error!("Failed to register currency '{currency_str}': {e}");
373 }
374 currency
375 }
376 };
377
378 let divisor = match margin.currency.as_str() {
380 "XBt" => 100_000_000.0, "USDt" | "LAMp" | "MAMUSd" | "RLUSd" => 1_000_000.0, _ => 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 AccountBalance::new(total, locked, free)
430}
431
432pub fn parse_account_state(
438 margin: &BitmexMarginMsg,
439 account_id: AccountId,
440 ts_init: UnixNanos,
441) -> anyhow::Result<AccountState> {
442 let balance = parse_account_balance(margin);
443 let balances = vec![balance];
444
445 let margins = Vec::new();
448
449 let account_type = AccountType::Margin;
450 let is_reported = true;
451 let event_id = UUID4::new();
452 let ts_event =
453 UnixNanos::from(margin.timestamp.timestamp_nanos_opt().unwrap_or_default() as u64);
454
455 Ok(AccountState::new(
456 account_id,
457 account_type,
458 balances,
459 margins,
460 is_reported,
461 event_id,
462 ts_event,
463 ts_init,
464 None,
465 ))
466}
467
468#[cfg(test)]
469mod tests {
470 use chrono::TimeZone;
471 use nautilus_model::{instruments::CurrencyPair, types::fixed::FIXED_PRECISION};
472 use rstest::rstest;
473 use ustr::Ustr;
474
475 use super::*;
476
477 #[rstest]
478 fn test_clean_reason_strips_nautilus_trader() {
479 assert_eq!(
480 clean_reason(
481 "Canceled: Order had execInst of ParticipateDoNotInitiate\nNautilusTrader"
482 ),
483 "Canceled: Order had execInst of ParticipateDoNotInitiate"
484 );
485
486 assert_eq!(clean_reason("Some error\nNautilusTrader"), "Some error");
487 assert_eq!(
488 clean_reason("Multiple lines\nSome content\nNautilusTrader"),
489 "Multiple lines\nSome content"
490 );
491 assert_eq!(clean_reason("No identifier here"), "No identifier here");
492 assert_eq!(clean_reason(" \nNautilusTrader "), "");
493 }
494
495 fn make_test_spot_instrument(size_increment: f64, size_precision: u8) -> InstrumentAny {
496 let instrument_id = InstrumentId::from("SOLUSDT.BITMEX");
497 let raw_symbol = Symbol::from("SOLUSDT");
498 let base_currency = Currency::from("SOL");
499 let quote_currency = Currency::from("USDT");
500 let price_precision = 2;
501 let price_increment = Price::new(0.01, price_precision);
502 let size_increment = Quantity::new(size_increment, size_precision);
503 let instrument = CurrencyPair::new(
504 instrument_id,
505 raw_symbol,
506 base_currency,
507 quote_currency,
508 price_precision,
509 size_precision,
510 price_increment,
511 size_increment,
512 None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::from(0),
525 UnixNanos::from(0),
526 );
527 InstrumentAny::CurrencyPair(instrument)
528 }
529
530 #[rstest]
531 fn test_quantity_to_u32_scaled() {
532 let instrument = make_test_spot_instrument(0.0001, 4);
533 let qty = Quantity::new(0.1, 4);
534 assert_eq!(quantity_to_u32(&qty, &instrument), 1_000);
535 }
536
537 #[rstest]
538 fn test_parse_contracts_quantity_scaled() {
539 let instrument = make_test_spot_instrument(0.0001, 4);
540 let qty = parse_contracts_quantity(1_000, &instrument);
541 assert!((qty.as_f64() - 0.1).abs() < 1e-9);
542 assert_eq!(qty.precision, 4);
543 }
544
545 #[rstest]
546 fn test_convert_contract_quantity_scaling() {
547 let max_scale = FIXED_PRECISION as u32;
548 let (contract_decimal, size_increment) =
549 derive_contract_decimal_and_increment(Some(10_000.0), max_scale).unwrap();
550 assert!((size_increment.as_f64() - 0.0001).abs() < 1e-12);
551
552 let lot_qty =
553 convert_contract_quantity(Some(1_000.0), contract_decimal, max_scale, "lot size")
554 .unwrap()
555 .unwrap();
556 assert!((lot_qty.as_f64() - 0.1).abs() < 1e-9);
557 assert_eq!(lot_qty.precision, 1);
558 }
559
560 #[rstest]
561 fn test_derive_contract_decimal_defaults_to_one() {
562 let max_scale = FIXED_PRECISION as u32;
563 let (contract_decimal, size_increment) =
564 derive_contract_decimal_and_increment(Some(0.0), max_scale).unwrap();
565 assert_eq!(contract_decimal, Decimal::ONE);
566 assert_eq!(size_increment.as_f64(), 1.0);
567 }
568
569 #[rstest]
570 fn test_parse_account_state() {
571 let margin_msg = BitmexMarginMsg {
572 account: 123456,
573 currency: Ustr::from("XBt"),
574 risk_limit: Some(1000000000),
575 amount: Some(5000000),
576 prev_realised_pnl: Some(100000),
577 gross_comm: Some(1000),
578 gross_open_cost: Some(200000),
579 gross_open_premium: None,
580 gross_exec_cost: None,
581 gross_mark_value: Some(210000),
582 risk_value: Some(50000),
583 init_margin: Some(20000),
584 maint_margin: Some(10000),
585 target_excess_margin: Some(5000),
586 realised_pnl: Some(100000),
587 unrealised_pnl: Some(10000),
588 wallet_balance: Some(5000000),
589 margin_balance: Some(5010000),
590 margin_leverage: Some(2.5),
591 margin_used_pcnt: Some(0.25),
592 excess_margin: Some(4990000),
593 available_margin: Some(4980000),
594 withdrawable_margin: Some(4900000),
595 maker_fee_discount: Some(0.1),
596 taker_fee_discount: Some(0.05),
597 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
598 foreign_margin_balance: None,
599 foreign_requirement: None,
600 };
601
602 let account_id = AccountId::new("BITMEX-001");
603 let ts_init = UnixNanos::from(1_000_000_000);
604
605 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
606
607 assert_eq!(account_state.account_id, account_id);
608 assert_eq!(account_state.account_type, AccountType::Margin);
609 assert_eq!(account_state.balances.len(), 1);
610 assert_eq!(account_state.margins.len(), 0); assert!(account_state.is_reported);
612
613 let xbt_balance = &account_state.balances[0];
614 assert_eq!(xbt_balance.currency, Currency::from("XBT"));
615 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); }
619
620 #[rstest]
621 fn test_parse_account_state_usdt() {
622 let margin_msg = BitmexMarginMsg {
623 account: 123456,
624 currency: Ustr::from("USDt"),
625 risk_limit: Some(1000000000),
626 amount: Some(10000000000), prev_realised_pnl: None,
628 gross_comm: None,
629 gross_open_cost: None,
630 gross_open_premium: None,
631 gross_exec_cost: None,
632 gross_mark_value: None,
633 risk_value: None,
634 init_margin: Some(500000), maint_margin: Some(250000), target_excess_margin: None,
637 realised_pnl: None,
638 unrealised_pnl: None,
639 wallet_balance: Some(10000000000),
640 margin_balance: Some(10000000000),
641 margin_leverage: None,
642 margin_used_pcnt: None,
643 excess_margin: None,
644 available_margin: Some(9500000000), withdrawable_margin: None,
646 maker_fee_discount: None,
647 taker_fee_discount: None,
648 timestamp: chrono::Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap(),
649 foreign_margin_balance: None,
650 foreign_requirement: None,
651 };
652
653 let account_id = AccountId::new("BITMEX-001");
654 let ts_init = UnixNanos::from(1_000_000_000);
655
656 let account_state = parse_account_state(&margin_msg, account_id, ts_init).unwrap();
657
658 let usdt_balance = &account_state.balances[0];
659 assert_eq!(usdt_balance.currency, Currency::USDT());
660 assert_eq!(usdt_balance.total.as_f64(), 10000.0);
661 assert_eq!(usdt_balance.free.as_f64(), 9500.0);
662 assert_eq!(usdt_balance.locked.as_f64(), 500.0);
663
664 assert_eq!(account_state.margins.len(), 0); }
666
667 #[rstest]
668 fn test_parse_margin_message_with_missing_fields() {
669 let margin_msg = BitmexMarginMsg {
671 account: 123456,
672 currency: Ustr::from("XBt"),
673 risk_limit: None,
674 amount: None,
675 prev_realised_pnl: None,
676 gross_comm: None,
677 gross_open_cost: None,
678 gross_open_premium: None,
679 gross_exec_cost: None,
680 gross_mark_value: None,
681 risk_value: None,
682 init_margin: None, maint_margin: None, target_excess_margin: None,
685 realised_pnl: None,
686 unrealised_pnl: None,
687 wallet_balance: Some(100000),
688 margin_balance: None,
689 margin_leverage: None,
690 margin_used_pcnt: None,
691 excess_margin: None,
692 available_margin: Some(95000),
693 withdrawable_margin: None,
694 maker_fee_discount: None,
695 taker_fee_discount: None,
696 timestamp: chrono::Utc::now(),
697 foreign_margin_balance: None,
698 foreign_requirement: None,
699 };
700
701 let account_id = AccountId::new("BITMEX-123456");
702 let ts_init = UnixNanos::from(1_000_000_000);
703
704 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
705 .expect("Should parse even with missing margin fields");
706
707 assert_eq!(account_state.balances.len(), 1);
709 assert_eq!(account_state.margins.len(), 0); }
711
712 #[rstest]
713 fn test_parse_margin_message_with_only_available_margin() {
714 let margin_msg = BitmexMarginMsg {
716 account: 1667725,
717 currency: Ustr::from("USDt"),
718 risk_limit: None,
719 amount: None,
720 prev_realised_pnl: None,
721 gross_comm: None,
722 gross_open_cost: None,
723 gross_open_premium: None,
724 gross_exec_cost: None,
725 gross_mark_value: None,
726 risk_value: None,
727 init_margin: None,
728 maint_margin: None,
729 target_excess_margin: None,
730 realised_pnl: None,
731 unrealised_pnl: None,
732 wallet_balance: None, margin_balance: None, margin_leverage: None,
735 margin_used_pcnt: None,
736 excess_margin: None,
737 available_margin: Some(107859036), withdrawable_margin: None,
739 maker_fee_discount: None,
740 taker_fee_discount: None,
741 timestamp: chrono::Utc::now(),
742 foreign_margin_balance: None,
743 foreign_requirement: None,
744 };
745
746 let account_id = AccountId::new("BITMEX-1667725");
747 let ts_init = UnixNanos::from(1_000_000_000);
748
749 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
750 .expect("Should handle case with only available_margin");
751
752 let balance = &account_state.balances[0];
754 assert_eq!(balance.currency, Currency::USDT());
755 assert_eq!(balance.total.as_f64(), 107.859036); assert_eq!(balance.free.as_f64(), 107.859036);
757 assert_eq!(balance.locked.as_f64(), 0.0);
758
759 assert_eq!(balance.total, balance.locked + balance.free);
761 }
762
763 #[rstest]
764 fn test_parse_margin_available_exceeds_wallet() {
765 let margin_msg = BitmexMarginMsg {
767 account: 123456,
768 currency: Ustr::from("XBt"),
769 risk_limit: None,
770 amount: Some(70772),
771 prev_realised_pnl: None,
772 gross_comm: None,
773 gross_open_cost: None,
774 gross_open_premium: None,
775 gross_exec_cost: None,
776 gross_mark_value: None,
777 risk_value: None,
778 init_margin: Some(0),
779 maint_margin: Some(0),
780 target_excess_margin: None,
781 realised_pnl: None,
782 unrealised_pnl: None,
783 wallet_balance: Some(70772), margin_balance: None,
785 margin_leverage: None,
786 margin_used_pcnt: None,
787 excess_margin: None,
788 available_margin: Some(94381), withdrawable_margin: None,
790 maker_fee_discount: None,
791 taker_fee_discount: None,
792 timestamp: chrono::Utc::now(),
793 foreign_margin_balance: None,
794 foreign_requirement: None,
795 };
796
797 let account_id = AccountId::new("BITMEX-123456");
798 let ts_init = UnixNanos::from(1_000_000_000);
799
800 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
801 .expect("Should handle available > wallet case");
802
803 let balance = &account_state.balances[0];
805 assert_eq!(balance.currency, Currency::from("XBT"));
806 assert_eq!(balance.total.as_f64(), 0.00070772); assert_eq!(balance.free.as_f64(), 0.00070772); assert_eq!(balance.locked.as_f64(), 0.0);
809
810 assert_eq!(balance.total, balance.locked + balance.free);
812 }
813
814 #[rstest]
815 fn test_parse_margin_message_with_foreign_requirements() {
816 let margin_msg = BitmexMarginMsg {
818 account: 123456,
819 currency: Ustr::from("XBt"),
820 risk_limit: Some(1000000000),
821 amount: Some(100000000), prev_realised_pnl: None,
823 gross_comm: None,
824 gross_open_cost: None,
825 gross_open_premium: None,
826 gross_exec_cost: None,
827 gross_mark_value: None,
828 risk_value: None,
829 init_margin: None, maint_margin: None, target_excess_margin: None,
832 realised_pnl: None,
833 unrealised_pnl: None,
834 wallet_balance: Some(100000000),
835 margin_balance: Some(100000000),
836 margin_leverage: None,
837 margin_used_pcnt: None,
838 excess_margin: None,
839 available_margin: Some(95000000), withdrawable_margin: None,
841 maker_fee_discount: None,
842 taker_fee_discount: None,
843 timestamp: chrono::Utc::now(),
844 foreign_margin_balance: Some(100000000), foreign_requirement: Some(5000000), };
847
848 let account_id = AccountId::new("BITMEX-123456");
849 let ts_init = UnixNanos::from(1_000_000_000);
850
851 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
852 .expect("Failed to parse account state with foreign requirements");
853
854 let balance = &account_state.balances[0];
856 assert_eq!(balance.currency, Currency::from("XBT"));
857 assert_eq!(balance.total.as_f64(), 1.0);
858 assert_eq!(balance.free.as_f64(), 0.95);
859 assert_eq!(balance.locked.as_f64(), 0.05);
860
861 assert_eq!(account_state.margins.len(), 0);
863 }
864
865 #[rstest]
866 fn test_parse_margin_message_with_both_standard_and_foreign() {
867 let margin_msg = BitmexMarginMsg {
869 account: 123456,
870 currency: Ustr::from("XBt"),
871 risk_limit: Some(1000000000),
872 amount: Some(100000000), prev_realised_pnl: None,
874 gross_comm: None,
875 gross_open_cost: None,
876 gross_open_premium: None,
877 gross_exec_cost: None,
878 gross_mark_value: None,
879 risk_value: None,
880 init_margin: Some(2000000), maint_margin: Some(1000000), target_excess_margin: None,
883 realised_pnl: None,
884 unrealised_pnl: None,
885 wallet_balance: Some(100000000),
886 margin_balance: Some(100000000),
887 margin_leverage: None,
888 margin_used_pcnt: None,
889 excess_margin: None,
890 available_margin: Some(93000000), withdrawable_margin: None,
892 maker_fee_discount: None,
893 taker_fee_discount: None,
894 timestamp: chrono::Utc::now(),
895 foreign_margin_balance: Some(100000000),
896 foreign_requirement: Some(5000000), };
898
899 let account_id = AccountId::new("BITMEX-123456");
900 let ts_init = UnixNanos::from(1_000_000_000);
901
902 let account_state = parse_account_state(&margin_msg, account_id, ts_init)
903 .expect("Failed to parse account state with both margins");
904
905 let balance = &account_state.balances[0];
907 assert_eq!(balance.currency, Currency::from("XBT"));
908 assert_eq!(balance.total.as_f64(), 1.0);
909 assert_eq!(balance.free.as_f64(), 0.93);
910 assert_eq!(balance.locked.as_f64(), 0.07); assert_eq!(account_state.margins.len(), 0);
914 }
915}