1use std::{
31 fmt::Display,
32 hash::{Hash, Hasher},
33 ops::{Deref, DerefMut},
34};
35
36use ahash::AHashMap;
37use indexmap::IndexMap;
38use nautilus_core::correctness::{CorrectnessResultExt, FAILED, check_positive_decimal};
39use rust_decimal::Decimal;
40use serde::{Deserialize, Serialize};
41
42use crate::{
43 accounts::{
44 Account,
45 base::BaseAccount,
46 margin_model::{MarginModel, MarginModelHandle},
47 },
48 enums::{AccountType, InstrumentClass, OrderSide},
49 events::{AccountState, OrderFilled},
50 identifiers::InstrumentId,
51 instruments::{Instrument, InstrumentAny},
52 position::Position,
53 types::{
54 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
55 money::{MONEY_RAW_MAX, MONEY_RAW_MIN, MoneyRaw},
56 },
57};
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[cfg_attr(
62 feature = "python",
63 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
64)]
65#[cfg_attr(
66 feature = "python",
67 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
68)]
69pub struct MarginAccount {
70 pub base: BaseAccount,
72 pub leverages: AHashMap<InstrumentId, Decimal>,
74 pub margins: IndexMap<InstrumentId, MarginBalance>,
77 pub account_margins: IndexMap<Currency, MarginBalance>,
81 pub default_leverage: Decimal,
83 #[serde(skip, default = "MarginModelHandle::default")]
84 margin_model: MarginModelHandle,
85}
86
87impl MarginAccount {
88 #[must_use]
90 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
91 let (margins, account_margins) = split_event_margins(&event);
92
93 Self {
94 base: BaseAccount::new(event, calculate_account_state),
95 leverages: AHashMap::new(),
96 margins,
97 account_margins,
98 default_leverage: Decimal::ONE,
99 margin_model: MarginModelHandle::default(),
100 }
101 }
102
103 #[must_use]
104 pub(crate) fn clone_without_events(&self) -> Self {
105 Self {
106 base: self.base.clone_without_events(),
107 leverages: self.leverages.clone(),
108 margins: self.margins.clone(),
109 account_margins: self.account_margins.clone(),
110 default_leverage: self.default_leverage,
111 margin_model: self.margin_model.clone(),
112 }
113 }
114
115 pub fn set_margin_model(&mut self, model: MarginModelHandle) {
116 self.margin_model = model;
117 }
118
119 #[must_use]
120 pub const fn margin_model(&self) -> &MarginModelHandle {
121 &self.margin_model
122 }
123
124 pub fn set_default_leverage(&mut self, leverage: Decimal) {
130 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
131 self.default_leverage = leverage;
132 }
133
134 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
140 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
141 self.leverages.insert(instrument_id, leverage);
142 }
143
144 #[must_use]
145 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
146 *self
147 .leverages
148 .get(instrument_id)
149 .unwrap_or(&self.default_leverage)
150 }
151
152 #[must_use]
153 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
154 self.get_leverage(&instrument_id) == Decimal::ONE
155 }
156
157 #[must_use]
158 pub fn is_cash_account(&self) -> bool {
159 self.account_type == AccountType::Cash
160 }
161
162 #[must_use]
163 pub fn is_margin_account(&self) -> bool {
164 self.account_type == AccountType::Margin
165 }
166
167 #[must_use]
168 pub fn initial_margins(&self) -> IndexMap<InstrumentId, Money> {
169 self.margins
170 .values()
171 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.initial)))
172 .collect()
173 }
174
175 #[must_use]
176 pub fn maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
177 self.margins
178 .values()
179 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.maintenance)))
180 .collect()
181 }
182
183 #[must_use]
185 pub fn account_initial_margins(&self) -> IndexMap<Currency, Money> {
186 self.account_margins
187 .values()
188 .map(|margin| (margin.currency, margin.initial))
189 .collect()
190 }
191
192 #[must_use]
194 pub fn account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
195 self.account_margins
196 .values()
197 .map(|margin| (margin.currency, margin.maintenance))
198 .collect()
199 }
200
201 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
203 self.update_margin_component(instrument_id, margin_init, MarginComponent::Initial);
204 }
205
206 pub fn clear_initial_margin(&mut self, instrument_id: InstrumentId) {
208 self.clear_margin_component(instrument_id, MarginComponent::Initial);
209 }
210
211 #[must_use]
217 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
218 let margin_balance = self.margins.get(&instrument_id);
219 assert!(
220 margin_balance.is_some(),
221 "Cannot get margin_init when no margin_balance"
222 );
223 margin_balance.unwrap().initial
224 }
225
226 pub fn update_maintenance_margin(
228 &mut self,
229 instrument_id: InstrumentId,
230 margin_maintenance: Money,
231 ) {
232 self.update_margin_component(
233 instrument_id,
234 margin_maintenance,
235 MarginComponent::Maintenance,
236 );
237 }
238
239 pub fn clear_maintenance_margin(&mut self, instrument_id: InstrumentId) {
241 self.clear_margin_component(instrument_id, MarginComponent::Maintenance);
242 }
243
244 fn update_margin_component(
245 &mut self,
246 instrument_id: InstrumentId,
247 amount: Money,
248 component: MarginComponent,
249 ) {
250 let mut margin_balance = self
251 .margins
252 .get(&instrument_id)
253 .copied()
254 .unwrap_or_else(|| {
255 let zero = Money::zero(amount.currency);
256 MarginBalance::new(zero, zero, Some(instrument_id))
257 });
258 component.set(&mut margin_balance, amount);
259 self.margins.insert(instrument_id, margin_balance);
260
261 self.recalculate_balance(amount.currency);
262 }
263
264 fn clear_margin_component(&mut self, instrument_id: InstrumentId, component: MarginComponent) {
265 let Some(mut margin_balance) = self.margins.get(&instrument_id).copied() else {
266 return;
267 };
268
269 let currency = margin_balance.currency;
270 if component.sibling(&margin_balance).is_zero() {
271 self.margins.shift_remove(&instrument_id);
272 } else {
273 component.set(&mut margin_balance, Money::zero(currency));
274 self.margins.insert(instrument_id, margin_balance);
275 }
276
277 self.recalculate_balance(currency);
278 }
279
280 #[must_use]
286 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
287 let margin_balance = self.margins.get(&instrument_id);
288 assert!(
289 margin_balance.is_some(),
290 "Cannot get maintenance_margin when no margin_balance"
291 );
292 margin_balance.unwrap().maintenance
293 }
294
295 #[must_use]
297 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
298 self.margins.get(instrument_id).copied()
299 }
300
301 #[must_use]
303 pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
304 self.account_margins.get(currency).copied()
305 }
306
307 #[must_use]
309 pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
310 self.account_margins.get(currency).map(|m| m.initial)
311 }
312
313 #[must_use]
315 pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
316 self.account_margins.get(currency).map(|m| m.maintenance)
317 }
318
319 #[must_use]
322 pub fn total_initial_margin(&self, currency: Currency) -> Money {
323 self.total_margin_component(currency, MarginComponent::Initial)
324 }
325
326 #[must_use]
329 pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
330 self.total_margin_component(currency, MarginComponent::Maintenance)
331 }
332
333 fn total_margin_component(&self, currency: Currency, component: MarginComponent) -> Money {
334 let raw = self
335 .margins_in(currency)
336 .fold(0 as MoneyRaw, |raw, margin| {
337 raw.saturating_add(component.of(margin).raw())
338 });
339
340 Money::from_raw(clamp_money_raw(raw), currency)
341 }
342
343 fn margins_in(&self, currency: Currency) -> impl Iterator<Item = &MarginBalance> {
345 self.margins
346 .values()
347 .chain(self.account_margins.values())
348 .filter(move |margin| margin.currency == currency)
349 }
350
351 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
357 match margin_balance.instrument_id {
358 Some(instrument_id) => {
359 self.margins.insert(instrument_id, margin_balance);
360 }
361 None => {
362 self.account_margins
363 .insert(margin_balance.currency, margin_balance);
364 }
365 }
366 self.recalculate_balance(margin_balance.currency);
367 }
368
369 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
371 if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
372 self.recalculate_balance(margin_balance.currency);
373 }
374 }
375
376 pub fn clear_account_margin(&mut self, currency: Currency) {
378 if self.account_margins.shift_remove(¤cy).is_some() {
379 self.recalculate_balance(currency);
380 }
381 }
382
383 pub fn calculate_initial_margin<T: Instrument>(
392 &self,
393 instrument: &T,
394 quantity: Quantity,
395 price: Price,
396 use_quote_for_inverse: Option<bool>,
397 ) -> anyhow::Result<Money> {
398 let leverage = self.get_leverage(&instrument.id());
399 self.margin_model.calculate_initial_margin(
400 instrument,
401 quantity,
402 price,
403 leverage,
404 use_quote_for_inverse,
405 )
406 }
407
408 pub fn calculate_maintenance_margin<T: Instrument>(
416 &self,
417 instrument: &T,
418 quantity: Quantity,
419 price: Price,
420 use_quote_for_inverse: Option<bool>,
421 ) -> anyhow::Result<Money> {
422 let leverage = self.get_leverage(&instrument.id());
423 self.margin_model.calculate_maintenance_margin(
424 instrument,
425 quantity,
426 price,
427 leverage,
428 use_quote_for_inverse,
429 )
430 }
431
432 pub fn recalculate_balance(&mut self, currency: Currency) {
437 let current_balance = if let Some(balance) = self.balances.get(¤cy) {
438 *balance
439 } else {
440 log::debug!("Cannot recalculate balance when no current balance for {currency}");
444 return;
445 };
446
447 let total_margin_raw = self
450 .margins_in(currency)
451 .try_fold(0, |raw: MoneyRaw, margin| {
452 raw.checked_add(margin.initial.raw())?
453 .checked_add(margin.maintenance.raw())
454 });
455
456 let total_margin = total_margin_raw.map_or_else(
457 || {
458 log::error!(
459 "Cannot total {currency} margins: the sum exceeded Money bounds; reserving the full balance"
460 );
461 MONEY_RAW_MAX
462 },
463 clamp_money_raw,
464 );
465
466 let mut total_margin = Money::from_raw(total_margin, currency);
470 if total_margin > current_balance.total {
471 total_margin = current_balance.total.max(Money::zero(currency));
472 }
473
474 let total_free = current_balance.total - total_margin;
475
476 let new_balance = AccountBalance::new(current_balance.total, total_margin, total_free);
477 self.balances.insert(currency, new_balance);
478 }
479}
480
481#[derive(Clone, Copy)]
483enum MarginComponent {
484 Initial,
485 Maintenance,
486}
487
488impl MarginComponent {
489 const fn of(self, margin: &MarginBalance) -> Money {
490 match self {
491 Self::Initial => margin.initial,
492 Self::Maintenance => margin.maintenance,
493 }
494 }
495
496 const fn sibling(self, margin: &MarginBalance) -> Money {
498 match self {
499 Self::Initial => margin.maintenance,
500 Self::Maintenance => margin.initial,
501 }
502 }
503
504 const fn set(self, margin: &mut MarginBalance, amount: Money) {
505 match self {
506 Self::Initial => margin.initial = amount,
507 Self::Maintenance => margin.maintenance = amount,
508 }
509 }
510}
511
512fn split_event_margins(
513 event: &AccountState,
514) -> (
515 IndexMap<InstrumentId, MarginBalance>,
516 IndexMap<Currency, MarginBalance>,
517) {
518 let mut per_instrument: IndexMap<InstrumentId, MarginBalance> = IndexMap::new();
519 let mut per_currency: IndexMap<Currency, MarginBalance> = IndexMap::new();
520
521 for margin in &event.margins {
522 match margin.instrument_id {
523 Some(instrument_id) => {
524 per_instrument.insert(instrument_id, *margin);
525 }
526 None => {
527 per_currency.insert(margin.currency, *margin);
528 }
529 }
530 }
531
532 (per_instrument, per_currency)
533}
534
535#[inline]
536fn clamp_money_raw(raw: MoneyRaw) -> MoneyRaw {
537 raw.clamp(MONEY_RAW_MIN, MONEY_RAW_MAX)
538}
539
540impl Deref for MarginAccount {
541 type Target = BaseAccount;
542
543 fn deref(&self) -> &Self::Target {
544 &self.base
545 }
546}
547
548impl DerefMut for MarginAccount {
549 fn deref_mut(&mut self) -> &mut Self::Target {
550 &mut self.base
551 }
552}
553
554impl Account for MarginAccount {
555 impl_account_base_members!();
556
557 fn is_cash_account(&self) -> bool {
558 self.account_type == AccountType::Cash
559 }
560
561 fn is_margin_account(&self) -> bool {
562 self.account_type == AccountType::Margin
563 }
564
565 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
566 self.check_event_account_id(&event)?;
567
568 let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
569 let (per_instrument, per_currency) = split_event_margins(&event);
570 self.base_apply(event);
571
572 if !skip_margin_routing {
573 self.margins = per_instrument;
574 self.account_margins = per_currency;
575 }
576 Ok(())
577 }
578
579 fn calculate_balance_locked(
580 &self,
581 instrument: &InstrumentAny,
582 side: OrderSide,
583 quantity: Quantity,
584 price: Price,
585 use_quote_for_inverse: Option<bool>,
586 ) -> anyhow::Result<Money> {
587 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
588 }
589
590 fn calculate_pnls(
591 &self,
592 instrument: &InstrumentAny,
593 fill: &OrderFilled,
594 position: Option<Position>,
595 ) -> anyhow::Result<Vec<Money>> {
596 let mut pnls: Vec<Money> = Vec::new();
597
598 let instrument_class = instrument.instrument_class();
600
601 if matches!(
602 instrument_class,
603 InstrumentClass::Option
604 | InstrumentClass::OptionSpread
605 | InstrumentClass::BinaryOption
606 | InstrumentClass::Warrant
607 ) {
608 let notional =
609 instrument.try_calculate_notional_value(fill.last_qty, fill.last_px, None)?;
610 let pnl = if fill.order_side == OrderSide::Buy {
611 -notional
612 } else {
613 notional
614 };
615 pnls.push(pnl);
616 return Ok(pnls);
617 }
618
619 if let Some(ref pos) = position
621 && pos.quantity.is_positive()
622 && pos.entry != fill.order_side
623 {
624 let mut pnl_quantity = fill.last_qty.min(pos.quantity);
627 pnl_quantity.precision = fill.last_qty.precision;
628 let pnl =
629 pos.try_calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity)?;
630 pnls.push(pnl);
631 }
632
633 Ok(pnls)
634 }
635}
636
637impl PartialEq for MarginAccount {
638 fn eq(&self, other: &Self) -> bool {
639 self.id == other.id
640 }
641}
642
643impl Eq for MarginAccount {}
644
645impl Display for MarginAccount {
646 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
647 write!(
648 f,
649 "MarginAccount(id={}, type={}, base={})",
650 self.id,
651 self.account_type,
652 self.base_currency.map_or_else(
653 || "None".to_string(),
654 |base_currency| format!("{}", base_currency.code)
655 ),
656 )
657 }
658}
659
660impl Hash for MarginAccount {
661 fn hash<H: Hasher>(&self, state: &mut H) {
662 self.id.hash(state);
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use indexmap::IndexMap;
669 use nautilus_core::UnixNanos;
670 use rstest::rstest;
671 use rust_decimal::Decimal;
672
673 use crate::{
674 accounts::{
675 Account, MarginAccount,
676 margin_model::{MarginModel, MarginModelHandle},
677 stubs::*,
678 },
679 enums::{AccountType, OrderSide, OrderType},
680 events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
681 identifiers::{
682 AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
683 stubs::{uuid4, *},
684 },
685 instruments::{
686 CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
687 stubs::{binary_option, option_contract_appl, *},
688 },
689 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
690 position::Position,
691 types::{
692 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
693 money::{MONEY_RAW_MAX, MONEY_RAW_MIN},
694 },
695 };
696
697 struct CustomMarginModel;
698
699 impl MarginModel for CustomMarginModel {
700 fn name(&self) -> &'static str {
701 "custom"
702 }
703
704 fn calculate_initial_margin(
705 &self,
706 _instrument: &dyn Instrument,
707 _quantity: Quantity,
708 _price: Price,
709 _leverage: Decimal,
710 _use_quote_for_inverse: Option<bool>,
711 ) -> anyhow::Result<Money> {
712 Ok(Money::from("12.34 USD"))
713 }
714
715 fn calculate_maintenance_margin(
716 &self,
717 _instrument: &dyn Instrument,
718 _quantity: Quantity,
719 _price: Price,
720 _leverage: Decimal,
721 _use_quote_for_inverse: Option<bool>,
722 ) -> anyhow::Result<Money> {
723 Ok(Money::from("5.67 USD"))
724 }
725 }
726
727 #[rstest]
728 fn test_display(margin_account: MarginAccount) {
729 assert_eq!(
730 margin_account.to_string(),
731 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
732 );
733 }
734
735 #[rstest]
736 fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
737 assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
738 assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
739 }
740
741 #[rstest]
742 fn test_base_account_properties(
743 margin_account: MarginAccount,
744 margin_account_state: AccountState,
745 ) {
746 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
747 assert_eq!(
748 margin_account.last_event(),
749 Some(margin_account_state.clone())
750 );
751 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
752 assert_eq!(margin_account.event_count(), 1);
753 assert_eq!(
754 margin_account.balance_total(None),
755 Some(Money::from("1525000 USD"))
756 );
757 assert_eq!(
758 margin_account.balance_free(None),
759 Some(Money::from("1500000 USD"))
760 );
761 assert_eq!(
762 margin_account.balance_locked(None),
763 Some(Money::from("25000 USD"))
764 );
765 let mut balances_total_expected = IndexMap::new();
766 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
767 assert_eq!(margin_account.balances_total(), balances_total_expected);
768 let mut balances_free_expected = IndexMap::new();
769 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
770 assert_eq!(margin_account.balances_free(), balances_free_expected);
771 let mut balances_locked_expected = IndexMap::new();
772 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
773 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
774 let margin_balance = margin_account_state.margins[0];
775 let instrument_id = margin_balance
776 .instrument_id
777 .expect("stub margin balance carries a concrete instrument_id");
778 let mut initial_margins_expected = IndexMap::new();
779 initial_margins_expected.insert(instrument_id, margin_balance.initial);
780 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
781 let mut maintenance_margins_expected = IndexMap::new();
782 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
783 assert_eq!(
784 margin_account.maintenance_margins(),
785 maintenance_margins_expected
786 );
787 }
788
789 #[rstest]
790 fn test_set_default_leverage(mut margin_account: MarginAccount) {
791 assert_eq!(margin_account.default_leverage, Decimal::ONE);
792 margin_account.set_default_leverage(Decimal::from(10));
793 assert_eq!(margin_account.default_leverage, Decimal::from(10));
794 }
795
796 #[rstest]
797 fn test_get_leverage_default_leverage(
798 margin_account: MarginAccount,
799 instrument_id_aud_usd_sim: InstrumentId,
800 ) {
801 assert_eq!(
802 margin_account.get_leverage(&instrument_id_aud_usd_sim),
803 Decimal::ONE
804 );
805 }
806
807 #[rstest]
808 fn test_set_leverage(
809 mut margin_account: MarginAccount,
810 instrument_id_aud_usd_sim: InstrumentId,
811 ) {
812 assert_eq!(margin_account.leverages.len(), 0);
813 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
814 assert_eq!(margin_account.leverages.len(), 1);
815 assert_eq!(
816 margin_account.get_leverage(&instrument_id_aud_usd_sim),
817 Decimal::from(10)
818 );
819 }
820
821 #[rstest]
822 fn test_is_unleveraged_with_leverage_returns_false(
823 mut margin_account: MarginAccount,
824 instrument_id_aud_usd_sim: InstrumentId,
825 ) {
826 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
827 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
828 }
829
830 #[rstest]
831 fn test_is_unleveraged_with_no_leverage_returns_true(
832 mut margin_account: MarginAccount,
833 instrument_id_aud_usd_sim: InstrumentId,
834 ) {
835 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
836 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
837 }
838
839 #[rstest]
840 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
841 margin_account: MarginAccount,
842 instrument_id_aud_usd_sim: InstrumentId,
843 ) {
844 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
845 }
846
847 #[rstest]
848 fn test_update_margin_init(
849 mut margin_account: MarginAccount,
850 instrument_id_aud_usd_sim: InstrumentId,
851 ) {
852 assert_eq!(margin_account.margins.len(), 1);
853 let margin = Money::from("10000 USD");
854 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
855 assert_eq!(
856 margin_account.initial_margin(instrument_id_aud_usd_sim),
857 margin
858 );
859 assert_eq!(margin_account.margins.len(), 2);
860 assert_eq!(
861 margin_account
862 .margins
863 .get(&instrument_id_aud_usd_sim)
864 .expect("AUD/USD margin should exist")
865 .initial,
866 margin
867 );
868 assert_eq!(
869 margin_account
870 .margins
871 .get(&instrument_id_aud_usd_sim)
872 .expect("AUD/USD margin should exist")
873 .maintenance,
874 Money::zero(margin.currency)
875 );
876 }
877
878 #[rstest]
879 fn test_update_margin_maintenance(
880 mut margin_account: MarginAccount,
881 instrument_id_aud_usd_sim: InstrumentId,
882 ) {
883 let margin = Money::from("10000 USD");
884 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
885 assert_eq!(
886 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
887 margin
888 );
889 assert_eq!(margin_account.margins.len(), 2);
890 assert_eq!(
891 margin_account
892 .margins
893 .get(&instrument_id_aud_usd_sim)
894 .expect("AUD/USD margin should exist")
895 .maintenance,
896 margin
897 );
898 assert_eq!(
899 margin_account
900 .margins
901 .get(&instrument_id_aud_usd_sim)
902 .expect("AUD/USD margin should exist")
903 .initial,
904 Money::zero(margin.currency)
905 );
906 assert_eq!(
908 margin_account.balance_locked(Some(Currency::USD())),
909 Some(Money::from("35000 USD"))
910 );
911 assert_eq!(
912 margin_account.balance_free(Some(Currency::USD())),
913 Some(Money::from("1490000 USD"))
914 );
915 }
916
917 #[rstest]
918 fn test_clear_initial_margin_preserves_maintenance(
919 mut margin_account: MarginAccount,
920 instrument_id_aud_usd_sim: InstrumentId,
921 ) {
922 let usd = Currency::USD();
923 margin_account.update_margin(MarginBalance::new(
924 Money::from("1000 USD"),
925 Money::from("500 USD"),
926 Some(instrument_id_aud_usd_sim),
927 ));
928
929 assert_eq!(
930 margin_account.balance_locked(Some(usd)),
931 Some(Money::from("26500 USD"))
932 );
933
934 margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
935
936 let margin = margin_account
937 .margin(&instrument_id_aud_usd_sim)
938 .expect("margin should retain non-zero maintenance");
939 assert_eq!(margin.initial, Money::from("0 USD"));
940 assert_eq!(margin.maintenance, Money::from("500 USD"));
941 assert_eq!(
942 margin_account.balance_locked(Some(usd)),
943 Some(Money::from("25500 USD"))
944 );
945 assert_eq!(
946 margin_account.balance_free(Some(usd)),
947 Some(Money::from("1499500 USD"))
948 );
949 assert_eq!(
950 margin_account.balance_total(Some(usd)),
951 Some(Money::from("1525000 USD"))
952 );
953 }
954
955 #[rstest]
956 fn test_clear_maintenance_margin_removes_empty_entry(
957 mut margin_account: MarginAccount,
958 instrument_id_aud_usd_sim: InstrumentId,
959 ) {
960 let usd = Currency::USD();
961 margin_account.update_margin(MarginBalance::new(
962 Money::from("0 USD"),
963 Money::from("500 USD"),
964 Some(instrument_id_aud_usd_sim),
965 ));
966
967 assert_eq!(
968 margin_account.balance_locked(Some(usd)),
969 Some(Money::from("25500 USD"))
970 );
971
972 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
973
974 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
975 assert_eq!(
976 margin_account.balance_locked(Some(usd)),
977 Some(Money::from("25000 USD"))
978 );
979 assert_eq!(
980 margin_account.balance_free(Some(usd)),
981 Some(Money::from("1500000 USD"))
982 );
983 }
984
985 #[rstest]
986 fn test_clear_maintenance_margin_preserves_initial(
987 mut margin_account: MarginAccount,
988 instrument_id_aud_usd_sim: InstrumentId,
989 ) {
990 let usd = Currency::USD();
991 margin_account.update_margin(MarginBalance::new(
992 Money::from("1000 USD"),
993 Money::from("500 USD"),
994 Some(instrument_id_aud_usd_sim),
995 ));
996
997 assert_eq!(
998 margin_account.balance_locked(Some(usd)),
999 Some(Money::from("26500 USD"))
1000 );
1001
1002 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1003
1004 let margin = margin_account
1005 .margin(&instrument_id_aud_usd_sim)
1006 .expect("margin should retain non-zero initial");
1007 assert_eq!(margin.initial, Money::from("1000 USD"));
1008 assert_eq!(margin.maintenance, Money::from("0 USD"));
1009 assert_eq!(
1010 margin_account.balance_locked(Some(usd)),
1011 Some(Money::from("26000 USD"))
1012 );
1013 assert_eq!(
1014 margin_account.balance_free(Some(usd)),
1015 Some(Money::from("1499000 USD"))
1016 );
1017 }
1018
1019 #[rstest]
1020 fn test_apply_replaces_margin_balances_from_event(
1021 mut margin_account: MarginAccount,
1022 margin_account_state: AccountState,
1023 ) {
1024 let old_instrument_id = margin_account_state.margins[0]
1025 .instrument_id
1026 .expect("stub margin balance carries a concrete instrument_id");
1027 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1028 let event = AccountState::new(
1029 margin_account_state.account_id,
1030 AccountType::Margin,
1031 margin_account_state.balances.clone(),
1032 vec![MarginBalance::new(
1033 Money::from("12500 USD"),
1034 Money::from("25000 USD"),
1035 Some(new_instrument_id),
1036 )],
1037 true,
1038 uuid4(),
1039 1.into(),
1040 1.into(),
1041 margin_account_state.base_currency,
1042 );
1043
1044 margin_account.apply(event).unwrap();
1045
1046 assert_eq!(
1047 margin_account.initial_margin(new_instrument_id),
1048 Money::from("12500 USD")
1049 );
1050 assert_eq!(
1051 margin_account.maintenance_margin(new_instrument_id),
1052 Money::from("25000 USD")
1053 );
1054 assert!(margin_account.margin(&old_instrument_id).is_none());
1055 }
1056
1057 #[rstest]
1058 fn test_account_type_predicates(margin_account: MarginAccount) {
1059 assert!(!margin_account.is_cash_account());
1060 assert!(margin_account.is_margin_account());
1061 assert!(!Account::is_cash_account(&margin_account));
1062 assert!(Account::is_margin_account(&margin_account));
1063 }
1064
1065 #[rstest]
1066 fn test_equality_compares_account_ids(margin_account_state: AccountState) {
1067 let account = MarginAccount::new(margin_account_state.clone(), true);
1068 let same = MarginAccount::new(margin_account_state.clone(), true);
1069 let mut other_state = margin_account_state;
1070 other_state.account_id = AccountId::from("OTHER-001");
1071 let other = MarginAccount::new(other_state, true);
1072
1073 assert_eq!(account, same);
1074 assert_ne!(account, other);
1075 }
1076
1077 #[rstest]
1078 fn test_apply_routes_account_margins_when_event_has_no_balances(
1079 mut margin_account: MarginAccount,
1080 margin_account_state: AccountState,
1081 ) {
1082 let usd = Currency::USD();
1083
1084 let event = AccountState::new(
1085 margin_account_state.account_id,
1086 AccountType::Margin,
1087 vec![],
1088 vec![MarginBalance::new(
1089 Money::from("12500 USD"),
1090 Money::from("25000 USD"),
1091 None,
1092 )],
1093 true,
1094 uuid4(),
1095 1.into(),
1096 1.into(),
1097 margin_account_state.base_currency,
1098 );
1099
1100 margin_account.apply(event).unwrap();
1101
1102 assert_eq!(
1103 margin_account.account_initial_margins(),
1104 IndexMap::from([(usd, Money::from("12500 USD"))])
1105 );
1106 assert_eq!(
1107 margin_account.account_maintenance_margins(),
1108 IndexMap::from([(usd, Money::from("25000 USD"))])
1109 );
1110 }
1111
1112 #[rstest]
1113 fn test_apply_routes_account_margins_by_currency(
1114 mut margin_account: MarginAccount,
1115 margin_account_state: AccountState,
1116 ) {
1117 let usd = Currency::USD();
1118 let event = AccountState::new(
1119 margin_account_state.account_id,
1120 AccountType::Margin,
1121 margin_account_state.balances.clone(),
1122 vec![MarginBalance::new(
1123 Money::from("12500 USD"),
1124 Money::from("25000 USD"),
1125 None,
1126 )],
1127 true,
1128 uuid4(),
1129 1.into(),
1130 1.into(),
1131 margin_account_state.base_currency,
1132 );
1133
1134 margin_account.apply(event).unwrap();
1135
1136 assert!(margin_account.margins.is_empty());
1137 assert_eq!(margin_account.account_margins.len(), 1);
1138 assert_eq!(
1139 margin_account.account_initial_margin(&usd),
1140 Some(Money::from("12500 USD"))
1141 );
1142 assert_eq!(
1143 margin_account.account_maintenance_margin(&usd),
1144 Some(Money::from("25000 USD"))
1145 );
1146 assert_eq!(
1147 margin_account.total_initial_margin(usd),
1148 Money::from("12500 USD")
1149 );
1150 }
1151
1152 #[rstest]
1153 fn test_apply_empty_event_preserves_margin_balances(
1154 mut margin_account: MarginAccount,
1155 margin_account_state: AccountState,
1156 ) {
1157 let instrument_id = margin_account_state.margins[0]
1158 .instrument_id
1159 .expect("stub margin balance carries a concrete instrument_id");
1160 let initial_margin = margin_account.initial_margin(instrument_id);
1161 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1162
1163 let empty_event = AccountState::new(
1164 margin_account_state.account_id,
1165 AccountType::Margin,
1166 vec![],
1167 vec![],
1168 true,
1169 uuid4(),
1170 1.into(),
1171 1.into(),
1172 margin_account_state.base_currency,
1173 );
1174
1175 margin_account.apply(empty_event).unwrap();
1176
1177 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1178 assert_eq!(
1179 margin_account.maintenance_margin(instrument_id),
1180 maintenance_margin
1181 );
1182 assert_eq!(margin_account.event_count(), 2);
1183 }
1184
1185 #[rstest]
1186 fn test_calculate_margin_init_with_leverage(
1187 mut margin_account: MarginAccount,
1188 audusd_sim: CurrencyPair,
1189 ) {
1190 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1191 let result = margin_account
1192 .calculate_initial_margin(
1193 &audusd_sim,
1194 Quantity::from(100_000),
1195 Price::from("0.8000"),
1196 None,
1197 )
1198 .unwrap();
1199 assert_eq!(result, Money::from("48.00 USD"));
1200 }
1201
1202 #[rstest]
1203 fn test_custom_margin_model_through_account(
1204 mut margin_account: MarginAccount,
1205 audusd_sim: CurrencyPair,
1206 ) {
1207 margin_account.set_margin_model(MarginModelHandle::new(CustomMarginModel));
1208
1209 let initial = margin_account
1210 .calculate_initial_margin(
1211 &audusd_sim,
1212 Quantity::from(100_000),
1213 Price::from("0.8000"),
1214 None,
1215 )
1216 .unwrap();
1217 let maintenance = margin_account
1218 .calculate_maintenance_margin(
1219 &audusd_sim,
1220 Quantity::from(100_000),
1221 Price::from("0.8000"),
1222 None,
1223 )
1224 .unwrap();
1225
1226 assert_eq!(margin_account.margin_model().name(), "custom");
1227 assert_eq!(initial, Money::from("12.34 USD"));
1228 assert_eq!(maintenance, Money::from("5.67 USD"));
1229 }
1230
1231 #[rstest]
1232 fn test_calculate_margin_init_with_default_leverage(
1233 mut margin_account: MarginAccount,
1234 audusd_sim: CurrencyPair,
1235 ) {
1236 margin_account.set_default_leverage(Decimal::from(10));
1237 let result = margin_account
1238 .calculate_initial_margin(
1239 &audusd_sim,
1240 Quantity::from(100_000),
1241 Price::from("0.8"),
1242 None,
1243 )
1244 .unwrap();
1245 assert_eq!(result, Money::from("240.00 USD"));
1246 }
1247
1248 #[rstest]
1249 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1250 margin_account: MarginAccount,
1251 xbtusd_bitmex: CryptoPerpetual,
1252 ) {
1253 let result_use_quote_inverse_true = margin_account
1254 .calculate_initial_margin(
1255 &xbtusd_bitmex,
1256 Quantity::from(100_000),
1257 Price::from("11493.60"),
1258 Some(false),
1259 )
1260 .unwrap();
1261 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1262 let result_use_quote_inverse_false = margin_account
1263 .calculate_initial_margin(
1264 &xbtusd_bitmex,
1265 Quantity::from(100_000),
1266 Price::from("11493.60"),
1267 Some(true),
1268 )
1269 .unwrap();
1270 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1271 }
1272
1273 #[rstest]
1274 fn test_calculate_margin_maintenance_with_no_leverage(
1275 margin_account: MarginAccount,
1276 xbtusd_bitmex: CryptoPerpetual,
1277 ) {
1278 let result = margin_account
1279 .calculate_maintenance_margin(
1280 &xbtusd_bitmex,
1281 Quantity::from(100_000),
1282 Price::from("11493.60"),
1283 None,
1284 )
1285 .unwrap();
1286 assert_eq!(result, Money::from("0.03045173 BTC"));
1287 }
1288
1289 #[rstest]
1290 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1291 mut margin_account: MarginAccount,
1292 audusd_sim: CurrencyPair,
1293 ) {
1294 margin_account.set_default_leverage(Decimal::from(50));
1295 let result = margin_account
1296 .calculate_maintenance_margin(
1297 &audusd_sim,
1298 Quantity::from(1_000_000),
1299 Price::from("1"),
1300 None,
1301 )
1302 .unwrap();
1303 assert_eq!(result, Money::from("600.00 USD"));
1304 }
1305
1306 #[rstest]
1307 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1308 mut margin_account: MarginAccount,
1309 xbtusd_bitmex: CryptoPerpetual,
1310 ) {
1311 margin_account.set_default_leverage(Decimal::from(10));
1312 let result = margin_account
1313 .calculate_maintenance_margin(
1314 &xbtusd_bitmex,
1315 Quantity::from(100_000),
1316 Price::from("100000.00"),
1317 None,
1318 )
1319 .unwrap();
1320 assert_eq!(result, Money::from("0.00035000 BTC"));
1321 }
1322
1323 #[rstest]
1324 fn test_calculate_pnls_github_issue_2657() {
1325 let account_state = margin_account_state();
1327 let account = MarginAccount::new(account_state, false);
1328
1329 let btcusdt = currency_pair_btcusdt();
1331 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1332
1333 let fill1 = OrderFilledSpec::builder()
1335 .instrument_id(btcusdt_any.id())
1336 .client_order_id(ClientOrderId::from("O-1"))
1337 .venue_order_id(VenueOrderId::from("V-1"))
1338 .trade_id(TradeId::from("T-1"))
1339 .last_qty(Quantity::from("0.001"))
1340 .last_px(Price::from("50000.00"))
1341 .currency(btcusdt_any.quote_currency())
1342 .ts_event(UnixNanos::from(1_000_000_000))
1343 .position_id(PositionId::from("P-GITHUB-2657"))
1344 .build();
1345
1346 let position = Position::new(&btcusdt_any, fill1);
1347
1348 let fill2 = OrderFilledSpec::builder()
1350 .instrument_id(btcusdt_any.id())
1351 .client_order_id(ClientOrderId::from("O-2"))
1352 .venue_order_id(VenueOrderId::from("V-2"))
1353 .trade_id(TradeId::from("T-2"))
1354 .order_side(OrderSide::Sell)
1355 .last_qty(Quantity::from("0.002")) .last_px(Price::from("50075.00"))
1357 .currency(btcusdt_any.quote_currency())
1358 .ts_event(UnixNanos::from(2_000_000_000))
1359 .position_id(PositionId::from("P-GITHUB-2657"))
1360 .build();
1361
1362 let pnls = account
1364 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1365 .unwrap();
1366
1367 assert_eq!(pnls.len(), 1);
1369
1370 let expected_pnl = Money::from("0.075 USDT");
1373 assert_eq!(pnls[0], expected_pnl);
1374 }
1375
1376 #[rstest]
1377 #[should_panic(expected = "not positive")]
1378 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1379 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1380 }
1381
1382 #[rstest]
1383 #[should_panic(expected = "not positive")]
1384 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1385 margin_account.set_default_leverage(Decimal::ZERO);
1386 }
1387
1388 #[rstest]
1389 #[should_panic(expected = "not positive")]
1390 fn test_set_leverage_negative_panics(
1391 mut margin_account: MarginAccount,
1392 audusd_sim: CurrencyPair,
1393 ) {
1394 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1395 }
1396
1397 #[rstest]
1398 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1399 use nautilus_core::UnixNanos;
1400
1401 use crate::{
1402 events::order::spec::OrderFilledSpec,
1403 identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1404 instruments::InstrumentAny,
1405 position::Position,
1406 types::{Price, Quantity},
1407 };
1408
1409 let account_state = margin_account_state();
1411 let account = MarginAccount::new(account_state, false);
1412
1413 let btcusdt = currency_pair_btcusdt();
1415 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1416
1417 let fill1 = OrderFilledSpec::builder()
1419 .instrument_id(btcusdt.id)
1420 .client_order_id(ClientOrderId::from("O-1"))
1421 .venue_order_id(VenueOrderId::from("V-1"))
1422 .trade_id(TradeId::from("T-1"))
1423 .last_qty(Quantity::from("1.0"))
1424 .last_px(Price::from("50000.00"))
1425 .currency(btcusdt.quote_currency)
1426 .ts_event(UnixNanos::from(1_000_000_000))
1427 .position_id(PositionId::from("P-123456"))
1428 .build();
1429
1430 let position = Position::new(&btcusdt_any, fill1);
1431
1432 let fill2 = OrderFilledSpec::builder()
1434 .instrument_id(btcusdt.id)
1435 .client_order_id(ClientOrderId::from("O-2"))
1436 .venue_order_id(VenueOrderId::from("V-2"))
1437 .trade_id(TradeId::from("T-2"))
1438 .last_qty(Quantity::from("0.5"))
1439 .last_px(Price::from("51000.00"))
1440 .currency(btcusdt.quote_currency)
1441 .ts_event(UnixNanos::from(2_000_000_000))
1442 .position_id(PositionId::from("P-123456"))
1443 .build();
1444
1445 let pnls = account
1447 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1448 .unwrap();
1449
1450 assert_eq!(pnls.len(), 0);
1452 }
1453
1454 #[rstest]
1455 fn test_margin_accessor(
1456 mut margin_account: MarginAccount,
1457 instrument_id_aud_usd_sim: InstrumentId,
1458 ) {
1459 let margin_balance = MarginBalance::new(
1460 Money::from("1000 USD"),
1461 Money::from("500 USD"),
1462 Some(instrument_id_aud_usd_sim),
1463 );
1464
1465 margin_account.update_margin(margin_balance);
1466
1467 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1468 assert!(retrieved.is_some());
1469 let retrieved = retrieved.unwrap();
1470 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1471 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1472 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1473 }
1474
1475 #[rstest]
1476 fn test_clear_margin(
1477 mut margin_account: MarginAccount,
1478 instrument_id_aud_usd_sim: InstrumentId,
1479 ) {
1480 let usd = Currency::USD();
1481 let margin_balance = MarginBalance::new(
1482 Money::from("1000 USD"),
1483 Money::from("500 USD"),
1484 Some(instrument_id_aud_usd_sim),
1485 );
1486
1487 margin_account.update_margin(margin_balance);
1488 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1489 assert_eq!(
1490 margin_account.balance_locked(Some(usd)),
1491 Some(Money::from("26500 USD"))
1492 );
1493 assert_eq!(
1494 margin_account.balance_free(Some(usd)),
1495 Some(Money::from("1498500 USD"))
1496 );
1497
1498 margin_account.clear_margin(instrument_id_aud_usd_sim);
1499
1500 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1501 assert_eq!(
1502 margin_account.balance_locked(Some(usd)),
1503 Some(Money::from("25000 USD"))
1504 );
1505 assert_eq!(
1506 margin_account.balance_free(Some(usd)),
1507 Some(Money::from("1500000 USD"))
1508 );
1509 assert_eq!(
1510 margin_account.balance_total(Some(usd)),
1511 Some(Money::from("1525000 USD"))
1512 );
1513 }
1514
1515 #[rstest]
1516 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1517 let usd = Currency::USD();
1518 let margin_balance =
1519 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1520
1521 margin_account.update_margin(margin_balance);
1522
1523 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1524 assert_eq!(
1525 margin_account.account_initial_margin(&usd),
1526 Some(Money::from("200 USD"))
1527 );
1528 assert_eq!(
1529 margin_account.account_maintenance_margin(&usd),
1530 Some(Money::from("100 USD"))
1531 );
1532 assert_eq!(
1533 margin_account.balance_locked(Some(usd)),
1534 Some(Money::from("25300 USD"))
1535 );
1536 assert_eq!(
1537 margin_account.balance_free(Some(usd)),
1538 Some(Money::from("1499700 USD"))
1539 );
1540
1541 margin_account.clear_account_margin(usd);
1542
1543 assert!(margin_account.account_margin(&usd).is_none());
1544 assert_eq!(
1545 margin_account.balance_locked(Some(usd)),
1546 Some(Money::from("25000 USD"))
1547 );
1548 assert_eq!(
1549 margin_account.balance_free(Some(usd)),
1550 Some(Money::from("1500000 USD"))
1551 );
1552 }
1553
1554 #[rstest]
1555 #[should_panic(expected = "Cannot get margin_init when no margin_balance")]
1556 fn test_initial_margin_panics_when_absent(
1557 margin_account: MarginAccount,
1558 instrument_id_aud_usd_sim: InstrumentId,
1559 ) {
1560 let _ = margin_account.initial_margin(instrument_id_aud_usd_sim);
1561 }
1562
1563 #[rstest]
1564 #[should_panic(expected = "Cannot get maintenance_margin when no margin_balance")]
1565 fn test_maintenance_margin_panics_when_absent(
1566 margin_account: MarginAccount,
1567 instrument_id_aud_usd_sim: InstrumentId,
1568 ) {
1569 let _ = margin_account.maintenance_margin(instrument_id_aud_usd_sim);
1570 }
1571
1572 fn multi_currency_margin_account(reported: Money) -> MarginAccount {
1575 let state = AccountState::new(
1576 AccountId::from("BYBIT-001"),
1577 AccountType::Margin,
1578 vec![AccountBalance::new(
1579 reported,
1580 Money::zero(reported.currency),
1581 reported,
1582 )],
1583 Vec::new(),
1584 true,
1585 uuid4(),
1586 0.into(),
1587 0.into(),
1588 None,
1589 );
1590 MarginAccount::new(state, true)
1591 }
1592
1593 #[rstest]
1594 fn test_margin_update_leaves_unreported_currency_absent() {
1595 let usdt = Currency::USDT();
1596 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1597 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1598
1599 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1600
1601 assert_eq!(account.balance_total(Some(usdt)), None);
1602 assert_eq!(account.balance_locked(Some(usdt)), None);
1603 assert_eq!(account.balance_free(Some(usdt)), None);
1604 assert_eq!(
1605 account.balances.keys().copied().collect::<Vec<_>>(),
1606 vec![Currency::USD()]
1607 );
1608 assert_eq!(
1609 account.initial_margin(instrument_id),
1610 Money::from("5000 USDT")
1611 );
1612 assert_eq!(
1613 account.balance_free(Some(Currency::USD())),
1614 Some(Money::from("1000000 USD"))
1615 );
1616 }
1617
1618 #[rstest]
1619 fn test_margin_update_locks_reported_currency() {
1620 let usdt = Currency::USDT();
1621 let mut account = multi_currency_margin_account(Money::from("100000 USDT"));
1622 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1623
1624 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1625
1626 assert_eq!(
1627 account.balance_total(Some(usdt)),
1628 Some(Money::from("100000 USDT"))
1629 );
1630 assert_eq!(
1631 account.balance_locked(Some(usdt)),
1632 Some(Money::from("5000 USDT"))
1633 );
1634 assert_eq!(
1635 account.balance_free(Some(usdt)),
1636 Some(Money::from("95000 USDT"))
1637 );
1638 }
1639
1640 #[rstest]
1641 fn test_margin_update_locks_currency_reported_after_the_margin() {
1642 let usdt = Currency::USDT();
1643 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1644 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1645
1646 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1647 account
1648 .apply(AccountState::new(
1649 AccountId::from("BYBIT-001"),
1650 AccountType::Margin,
1651 vec![AccountBalance::new(
1652 Money::from("100000 USDT"),
1653 Money::zero(usdt),
1654 Money::from("100000 USDT"),
1655 )],
1656 Vec::new(),
1657 true,
1658 uuid4(),
1659 0.into(),
1660 0.into(),
1661 None,
1662 ))
1663 .unwrap();
1664 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1665
1666 assert_eq!(
1667 account.balance_locked(Some(usdt)),
1668 Some(Money::from("5000 USDT"))
1669 );
1670 assert_eq!(
1671 account.balance_free(Some(usdt)),
1672 Some(Money::from("95000 USDT"))
1673 );
1674 }
1675
1676 #[rstest]
1677 fn test_recalculate_balance_clamps_when_margin_exceeds_total() {
1678 let usdt = Currency::USDT();
1679 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1680
1681 account.update_initial_margin(
1682 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
1683 Money::from("1500 USDT"),
1684 );
1685
1686 assert_eq!(
1687 account.balance_total(Some(usdt)),
1688 Some(Money::from("1000 USDT"))
1689 );
1690 assert_eq!(
1691 account.balance_locked(Some(usdt)),
1692 Some(Money::from("1000 USDT"))
1693 );
1694 assert_eq!(
1695 account.balance_free(Some(usdt)),
1696 Some(Money::from("0 USDT"))
1697 );
1698 }
1699
1700 #[rstest]
1701 fn test_recalculate_balance_keeps_locked_non_negative_when_total_negative() {
1702 let usd = Currency::USD();
1703 let mut account = multi_currency_margin_account(Money::from("-1000 USD"));
1704 account.balances.insert(
1706 usd,
1707 AccountBalance::new(
1708 Money::from("-1000 USD"),
1709 Money::from("250 USD"),
1710 Money::from("-1250 USD"),
1711 ),
1712 );
1713
1714 account.update_initial_margin(InstrumentId::from("EURUSD.SIM"), Money::from("500 USD"));
1715
1716 assert_eq!(
1717 account.balance_total(Some(usd)),
1718 Some(Money::from("-1000 USD"))
1719 );
1720 assert_eq!(
1721 account.balance_locked(Some(usd)),
1722 Some(Money::from("0 USD"))
1723 );
1724 assert_eq!(
1725 account.balance_free(Some(usd)),
1726 Some(Money::from("-1000 USD"))
1727 );
1728 }
1729
1730 #[rstest]
1731 fn test_total_margin_sums_per_instrument_and_account_wide(
1732 mut margin_account: MarginAccount,
1733 instrument_id_aud_usd_sim: InstrumentId,
1734 ) {
1735 let usd = Currency::USD();
1736 let baseline_initial = margin_account.total_initial_margin(usd);
1737 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1738
1739 margin_account.update_margin(MarginBalance::new(
1740 Money::from("100 USD"),
1741 Money::from("50 USD"),
1742 Some(instrument_id_aud_usd_sim),
1743 ));
1744 margin_account.update_margin(MarginBalance::new(
1745 Money::from("200 USD"),
1746 Money::from("150 USD"),
1747 None,
1748 ));
1749
1750 assert_eq!(
1751 margin_account.total_initial_margin(usd),
1752 baseline_initial + Money::from("300 USD"),
1753 );
1754 assert_eq!(
1755 margin_account.total_maintenance_margin(usd),
1756 baseline_maintenance + Money::from("200 USD"),
1757 );
1758 }
1759
1760 #[rstest]
1761 fn test_recalculate_balance_reserves_full_balance_when_margins_cannot_be_totalled() {
1762 let usdt = Currency::USDT();
1763 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1764 let max = Money::from_raw(MONEY_RAW_MAX, usdt);
1765
1766 for symbol in ["ETHUSDT-PERP.BINANCE", "BTCUSDT-PERP.BINANCE"] {
1768 let instrument_id = InstrumentId::from(symbol);
1769 account.margins.insert(
1770 instrument_id,
1771 MarginBalance::new(max, max, Some(instrument_id)),
1772 );
1773 }
1774
1775 account.recalculate_balance(usdt);
1776
1777 assert_eq!(
1778 account.balance_total(Some(usdt)),
1779 Some(Money::from("1000 USDT"))
1780 );
1781 assert_eq!(
1782 account.balance_locked(Some(usdt)),
1783 Some(Money::from("1000 USDT"))
1784 );
1785 assert_eq!(
1786 account.balance_free(Some(usdt)),
1787 Some(Money::from("0 USDT"))
1788 );
1789 }
1790
1791 #[rstest]
1792 fn test_total_margin_clamps_domain_overflow(
1793 mut margin_account: MarginAccount,
1794 instrument_id_aud_usd_sim: InstrumentId,
1795 ) {
1796 let usd = Currency::USD();
1797 let max = Money::from_raw(MONEY_RAW_MAX, usd);
1798 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1799
1800 margin_account.margins.insert(
1801 instrument_id_aud_usd_sim,
1802 MarginBalance::new(max, max, Some(instrument_id_aud_usd_sim)),
1803 );
1804 margin_account.margins.insert(
1805 other_instrument,
1806 MarginBalance::new(max, max, Some(other_instrument)),
1807 );
1808
1809 assert_eq!(margin_account.total_initial_margin(usd), max);
1810 assert_eq!(margin_account.total_maintenance_margin(usd), max);
1811 }
1812
1813 #[rstest]
1814 fn test_total_margin_clamps_negative_domain_overflow(
1815 mut margin_account: MarginAccount,
1816 instrument_id_aud_usd_sim: InstrumentId,
1817 ) {
1818 let usd = Currency::USD();
1819 let min = Money::from_raw(MONEY_RAW_MIN, usd);
1820 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1821
1822 margin_account.margins.insert(
1823 instrument_id_aud_usd_sim,
1824 MarginBalance::new(min, min, Some(instrument_id_aud_usd_sim)),
1825 );
1826 margin_account.margins.insert(
1827 other_instrument,
1828 MarginBalance::new(min, min, Some(other_instrument)),
1829 );
1830
1831 assert_eq!(margin_account.total_initial_margin(usd), min);
1832 assert_eq!(margin_account.total_maintenance_margin(usd), min);
1833 }
1834
1835 #[rstest]
1836 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1837 let option = option_contract_appl();
1838 let option_any = InstrumentAny::OptionContract(option.clone());
1839
1840 let order = OrderTestBuilder::new(OrderType::Market)
1841 .instrument_id(option.id)
1842 .side(OrderSide::Buy)
1843 .quantity(Quantity::from("10"))
1844 .build();
1845
1846 let fill = TestOrderEventStubs::filled(
1847 &order,
1848 &option_any,
1849 None,
1850 Some(PositionId::new("P-OPT-001")),
1851 Some(Price::from("5.50")),
1852 None,
1853 None,
1854 None,
1855 None,
1856 Some(AccountId::from("SIM-001")),
1857 );
1858
1859 let fill_owned: crate::events::OrderFilled = fill.into();
1860 let pnls = margin_account
1861 .calculate_pnls(&option_any, &fill_owned, None)
1862 .unwrap();
1863
1864 assert_eq!(pnls.len(), 1);
1867 assert_eq!(pnls[0], Money::from("-55 USD"));
1868 }
1869
1870 #[rstest]
1871 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1872 let option = option_contract_appl();
1873 let option_any = InstrumentAny::OptionContract(option.clone());
1874
1875 let order = OrderTestBuilder::new(OrderType::Market)
1876 .instrument_id(option.id)
1877 .side(OrderSide::Sell)
1878 .quantity(Quantity::from("10"))
1879 .build();
1880
1881 let fill = TestOrderEventStubs::filled(
1882 &order,
1883 &option_any,
1884 None,
1885 Some(PositionId::new("P-OPT-002")),
1886 Some(Price::from("5.50")),
1887 None,
1888 None,
1889 None,
1890 None,
1891 Some(AccountId::from("SIM-001")),
1892 );
1893
1894 let fill_owned: crate::events::OrderFilled = fill.into();
1895 let pnls = margin_account
1896 .calculate_pnls(&option_any, &fill_owned, None)
1897 .unwrap();
1898
1899 assert_eq!(pnls.len(), 1);
1902 assert_eq!(pnls[0], Money::from("55 USD"));
1903 }
1904
1905 #[rstest]
1906 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1907 let binary = binary_option();
1908 let binary_any = InstrumentAny::BinaryOption(binary);
1909
1910 let order = OrderTestBuilder::new(OrderType::Market)
1911 .instrument_id(binary_any.id())
1912 .side(OrderSide::Buy)
1913 .quantity(Quantity::from("100"))
1914 .build();
1915
1916 let fill = TestOrderEventStubs::filled(
1917 &order,
1918 &binary_any,
1919 None,
1920 Some(PositionId::new("P-BIN-001")),
1921 Some(Price::from("0.65")),
1922 None,
1923 None,
1924 None,
1925 None,
1926 Some(AccountId::from("SIM-001")),
1927 );
1928
1929 let fill_owned: crate::events::OrderFilled = fill.into();
1930 let pnls = margin_account
1931 .calculate_pnls(&binary_any, &fill_owned, None)
1932 .unwrap();
1933
1934 assert_eq!(pnls.len(), 1);
1935 assert!(pnls[0].as_f64() < 0.0);
1936 }
1937}