1#![allow(dead_code)]
31
32use std::{
33 fmt::Display,
34 hash::{Hash, Hasher},
35 ops::{Deref, DerefMut},
36};
37
38use ahash::AHashMap;
39use indexmap::IndexMap;
40use nautilus_core::correctness::{CorrectnessResultExt, FAILED, check_positive_decimal};
41use rust_decimal::Decimal;
42use serde::{Deserialize, Serialize};
43
44use crate::{
45 accounts::{
46 Account,
47 base::BaseAccount,
48 margin_model::{MarginModel, MarginModelAny},
49 },
50 enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
51 events::{AccountState, OrderFilled},
52 identifiers::{AccountId, InstrumentId},
53 instruments::{Instrument, InstrumentAny},
54 position::Position,
55 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity, money::MoneyRaw},
56};
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[cfg_attr(
60 feature = "python",
61 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
62)]
63#[cfg_attr(
64 feature = "python",
65 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
66)]
67pub struct MarginAccount {
68 pub base: BaseAccount,
69 pub leverages: AHashMap<InstrumentId, Decimal>,
70 pub margins: IndexMap<InstrumentId, MarginBalance>,
73 pub account_margins: IndexMap<Currency, MarginBalance>,
77 pub default_leverage: Decimal,
78 #[serde(skip, default = "MarginModelAny::default")]
79 margin_model: MarginModelAny,
80}
81
82fn split_event_margins(
83 event: &AccountState,
84) -> (
85 IndexMap<InstrumentId, MarginBalance>,
86 IndexMap<Currency, MarginBalance>,
87) {
88 let mut per_instrument: IndexMap<InstrumentId, MarginBalance> = IndexMap::new();
89 let mut per_currency: IndexMap<Currency, MarginBalance> = IndexMap::new();
90
91 for margin in &event.margins {
92 match margin.instrument_id {
93 Some(instrument_id) => {
94 per_instrument.insert(instrument_id, *margin);
95 }
96 None => {
97 per_currency.insert(margin.currency, *margin);
98 }
99 }
100 }
101 (per_instrument, per_currency)
102}
103
104impl MarginAccount {
105 #[must_use]
107 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
108 let (margins, account_margins) = split_event_margins(&event);
109
110 Self {
111 base: BaseAccount::new(event, calculate_account_state),
112 leverages: AHashMap::new(),
113 margins,
114 account_margins,
115 default_leverage: Decimal::ONE,
116 margin_model: MarginModelAny::default(),
117 }
118 }
119
120 pub fn set_margin_model(&mut self, model: MarginModelAny) {
121 self.margin_model = model;
122 }
123
124 #[must_use]
125 pub const fn margin_model(&self) -> &MarginModelAny {
126 &self.margin_model
127 }
128
129 pub fn set_default_leverage(&mut self, leverage: Decimal) {
135 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
136 self.default_leverage = leverage;
137 }
138
139 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
145 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
146 self.leverages.insert(instrument_id, leverage);
147 }
148
149 #[must_use]
150 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
151 *self
152 .leverages
153 .get(instrument_id)
154 .unwrap_or(&self.default_leverage)
155 }
156
157 #[must_use]
158 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
159 self.get_leverage(&instrument_id) == Decimal::ONE
160 }
161
162 #[must_use]
163 pub fn is_cash_account(&self) -> bool {
164 self.account_type == AccountType::Cash
165 }
166
167 #[must_use]
168 pub fn is_margin_account(&self) -> bool {
169 self.account_type == AccountType::Margin
170 }
171
172 #[must_use]
173 pub fn initial_margins(&self) -> IndexMap<InstrumentId, Money> {
174 self.margins
175 .values()
176 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.initial)))
177 .collect()
178 }
179
180 #[must_use]
181 pub fn maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
182 self.margins
183 .values()
184 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.maintenance)))
185 .collect()
186 }
187
188 #[must_use]
190 pub fn account_initial_margins(&self) -> IndexMap<Currency, Money> {
191 self.account_margins
192 .values()
193 .map(|margin| (margin.currency, margin.initial))
194 .collect()
195 }
196
197 #[must_use]
199 pub fn account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
200 self.account_margins
201 .values()
202 .map(|margin| (margin.currency, margin.maintenance))
203 .collect()
204 }
205
206 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
208 let margin_balance = self.margins.get(&instrument_id);
209 if let Some(balance) = margin_balance {
210 let mut new_margin_balance = *balance;
212 new_margin_balance.initial = margin_init;
213 self.margins.insert(instrument_id, new_margin_balance);
214 } else {
215 self.margins.insert(
216 instrument_id,
217 MarginBalance::new(
218 margin_init,
219 Money::new(0.0, margin_init.currency),
220 Some(instrument_id),
221 ),
222 );
223 }
224 self.recalculate_balance(margin_init.currency);
225 }
226
227 #[must_use]
233 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
234 let margin_balance = self.margins.get(&instrument_id);
235 assert!(
236 margin_balance.is_some(),
237 "Cannot get margin_init when no margin_balance"
238 );
239 margin_balance.unwrap().initial
240 }
241
242 pub fn update_maintenance_margin(
244 &mut self,
245 instrument_id: InstrumentId,
246 margin_maintenance: Money,
247 ) {
248 let margin_balance = self.margins.get(&instrument_id);
249 if let Some(balance) = margin_balance {
250 let mut new_margin_balance = *balance;
252 new_margin_balance.maintenance = margin_maintenance;
253 self.margins.insert(instrument_id, new_margin_balance);
254 } else {
255 self.margins.insert(
256 instrument_id,
257 MarginBalance::new(
258 Money::new(0.0, margin_maintenance.currency),
259 margin_maintenance,
260 Some(instrument_id),
261 ),
262 );
263 }
264 self.recalculate_balance(margin_maintenance.currency);
265 }
266
267 #[must_use]
273 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
274 let margin_balance = self.margins.get(&instrument_id);
275 assert!(
276 margin_balance.is_some(),
277 "Cannot get maintenance_margin when no margin_balance"
278 );
279 margin_balance.unwrap().maintenance
280 }
281
282 #[must_use]
284 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
285 self.margins.get(instrument_id).copied()
286 }
287
288 #[must_use]
290 pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
291 self.account_margins.get(currency).copied()
292 }
293
294 #[must_use]
296 pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
297 self.account_margins.get(currency).map(|m| m.initial)
298 }
299
300 #[must_use]
302 pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
303 self.account_margins.get(currency).map(|m| m.maintenance)
304 }
305
306 #[must_use]
309 pub fn total_initial_margin(&self, currency: Currency) -> Money {
310 let mut raw: MoneyRaw = 0;
311
312 for margin in self.margins.values() {
313 if margin.currency == currency {
314 raw = raw.saturating_add(margin.initial.raw);
315 }
316 }
317
318 for margin in self.account_margins.values() {
319 if margin.currency == currency {
320 raw = raw.saturating_add(margin.initial.raw);
321 }
322 }
323
324 Money::from_raw(raw, currency)
325 }
326
327 #[must_use]
330 pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
331 let mut raw: MoneyRaw = 0;
332
333 for margin in self.margins.values() {
334 if margin.currency == currency {
335 raw = raw.saturating_add(margin.maintenance.raw);
336 }
337 }
338
339 for margin in self.account_margins.values() {
340 if margin.currency == currency {
341 raw = raw.saturating_add(margin.maintenance.raw);
342 }
343 }
344
345 Money::from_raw(raw, currency)
346 }
347
348 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
354 match margin_balance.instrument_id {
355 Some(instrument_id) => {
356 self.margins.insert(instrument_id, margin_balance);
357 }
358 None => {
359 self.account_margins
360 .insert(margin_balance.currency, margin_balance);
361 }
362 }
363 self.recalculate_balance(margin_balance.currency);
364 }
365
366 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
368 if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
369 self.recalculate_balance(margin_balance.currency);
370 }
371 }
372
373 pub fn clear_account_margin(&mut self, currency: Currency) {
375 if self.account_margins.shift_remove(¤cy).is_some() {
376 self.recalculate_balance(currency);
377 }
378 }
379
380 pub fn calculate_initial_margin<T: Instrument>(
389 &mut self,
390 instrument: &T,
391 quantity: Quantity,
392 price: Price,
393 use_quote_for_inverse: Option<bool>,
394 ) -> anyhow::Result<Money> {
395 let leverage = self.get_leverage(&instrument.id());
396 self.margin_model.calculate_initial_margin(
397 instrument,
398 quantity,
399 price,
400 leverage,
401 use_quote_for_inverse,
402 )
403 }
404
405 pub fn calculate_maintenance_margin<T: Instrument>(
413 &mut self,
414 instrument: &T,
415 quantity: Quantity,
416 price: Price,
417 use_quote_for_inverse: Option<bool>,
418 ) -> anyhow::Result<Money> {
419 let leverage = self.get_leverage(&instrument.id());
420 self.margin_model.calculate_maintenance_margin(
421 instrument,
422 quantity,
423 price,
424 leverage,
425 use_quote_for_inverse,
426 )
427 }
428
429 pub fn recalculate_balance(&mut self, currency: Currency) {
436 let current_balance = if let Some(balance) = self.balances.get(¤cy) {
437 *balance
438 } else {
439 let zero = Money::from_raw(0, currency);
442 AccountBalance::new(zero, zero, zero)
443 };
444
445 let mut total_margin: MoneyRaw = 0;
446
447 let accumulate = |raw: MoneyRaw, margin: &MarginBalance| -> MoneyRaw {
448 raw.checked_add(margin.initial.raw)
449 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
450 .unwrap_or_else(|| {
451 panic!(
452 "Margin calculation overflow for currency {}: total would exceed maximum",
453 currency.code
454 )
455 })
456 };
457
458 for margin in self.margins.values() {
459 if margin.currency == currency {
460 total_margin = accumulate(total_margin, margin);
461 }
462 }
463
464 for margin in self.account_margins.values() {
465 if margin.currency == currency {
466 total_margin = accumulate(total_margin, margin);
467 }
468 }
469
470 let total_free = if total_margin > current_balance.total.raw {
474 total_margin = current_balance.total.raw.max(0);
475 current_balance.total.raw - total_margin
476 } else {
477 current_balance.total.raw - total_margin
478 };
479
480 let new_balance = AccountBalance::new(
481 current_balance.total,
482 Money::from_raw(total_margin, currency),
483 Money::from_raw(total_free, currency),
484 );
485 self.balances.insert(currency, new_balance);
486 }
487}
488
489impl Deref for MarginAccount {
490 type Target = BaseAccount;
491
492 fn deref(&self) -> &Self::Target {
493 &self.base
494 }
495}
496
497impl DerefMut for MarginAccount {
498 fn deref_mut(&mut self) -> &mut Self::Target {
499 &mut self.base
500 }
501}
502
503impl Account for MarginAccount {
504 fn id(&self) -> AccountId {
505 self.id
506 }
507
508 fn account_type(&self) -> AccountType {
509 self.account_type
510 }
511
512 fn base_currency(&self) -> Option<Currency> {
513 self.base_currency
514 }
515
516 fn is_cash_account(&self) -> bool {
517 self.account_type == AccountType::Cash
518 }
519
520 fn is_margin_account(&self) -> bool {
521 self.account_type == AccountType::Margin
522 }
523
524 fn calculated_account_state(&self) -> bool {
525 false }
527
528 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
529 self.base_balance_total(currency)
530 }
531
532 fn balances_total(&self) -> IndexMap<Currency, Money> {
533 self.base_balances_total()
534 }
535
536 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
537 self.base_balance_free(currency)
538 }
539
540 fn balances_free(&self) -> IndexMap<Currency, Money> {
541 self.base_balances_free()
542 }
543
544 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
545 self.base_balance_locked(currency)
546 }
547
548 fn balances_locked(&self) -> IndexMap<Currency, Money> {
549 self.base_balances_locked()
550 }
551
552 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
553 self.base_balance(currency)
554 }
555
556 fn last_event(&self) -> Option<AccountState> {
557 self.base_last_event()
558 }
559
560 fn events(&self) -> Vec<AccountState> {
561 self.events.clone()
562 }
563
564 fn event_count(&self) -> usize {
565 self.events.len()
566 }
567
568 fn currencies(&self) -> Vec<Currency> {
569 self.balances.keys().copied().collect()
570 }
571
572 fn starting_balances(&self) -> IndexMap<Currency, Money> {
573 self.balances_starting.clone()
574 }
575
576 fn balances(&self) -> IndexMap<Currency, AccountBalance> {
577 self.balances.clone()
578 }
579
580 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
581 let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
582 let (per_instrument, per_currency) = split_event_margins(&event);
583 self.base_apply(event);
584
585 if !skip_margin_routing {
586 self.margins = per_instrument;
587 self.account_margins = per_currency;
588 }
589 Ok(())
590 }
591
592 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
593 self.base.base_purge_account_events(ts_now, lookback_secs);
594 }
595
596 fn calculate_balance_locked(
597 &mut self,
598 instrument: &InstrumentAny,
599 side: OrderSide,
600 quantity: Quantity,
601 price: Price,
602 use_quote_for_inverse: Option<bool>,
603 ) -> anyhow::Result<Money> {
604 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
605 }
606
607 fn calculate_pnls(
608 &self,
609 instrument: &InstrumentAny,
610 fill: &OrderFilled,
611 position: Option<Position>,
612 ) -> anyhow::Result<Vec<Money>> {
613 let mut pnls: Vec<Money> = Vec::new();
614
615 let instrument_class = instrument.instrument_class();
617
618 if matches!(
619 instrument_class,
620 InstrumentClass::Option
621 | InstrumentClass::OptionSpread
622 | InstrumentClass::BinaryOption
623 | InstrumentClass::Warrant
624 ) {
625 let notional = instrument.calculate_notional_value(fill.last_qty, fill.last_px, None);
626 let pnl = if fill.order_side == OrderSide::Buy {
627 Money::from_raw(-notional.raw, notional.currency)
628 } else {
629 notional
630 };
631 pnls.push(pnl);
632 return Ok(pnls);
633 }
634
635 if let Some(ref pos) = position
637 && pos.quantity.is_positive()
638 && pos.entry != fill.order_side
639 {
640 let pnl_quantity = Quantity::from_raw(
643 fill.last_qty.raw.min(pos.quantity.raw),
644 fill.last_qty.precision,
645 );
646 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
647 pnls.push(pnl);
648 }
649
650 Ok(pnls)
651 }
652
653 fn calculate_commission(
654 &self,
655 instrument: &InstrumentAny,
656 last_qty: Quantity,
657 last_px: Price,
658 liquidity_side: LiquiditySide,
659 use_quote_for_inverse: Option<bool>,
660 ) -> anyhow::Result<Money> {
661 self.base_calculate_commission(
662 instrument,
663 last_qty,
664 last_px,
665 liquidity_side,
666 use_quote_for_inverse,
667 )
668 }
669}
670
671impl PartialEq for MarginAccount {
672 fn eq(&self, other: &Self) -> bool {
673 self.id == other.id
674 }
675}
676
677impl Eq for MarginAccount {}
678
679impl Display for MarginAccount {
680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681 write!(
682 f,
683 "MarginAccount(id={}, type={}, base={})",
684 self.id,
685 self.account_type,
686 self.base_currency.map_or_else(
687 || "None".to_string(),
688 |base_currency| format!("{}", base_currency.code)
689 ),
690 )
691 }
692}
693
694impl Hash for MarginAccount {
695 fn hash<H: Hasher>(&self, state: &mut H) {
696 self.id.hash(state);
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use indexmap::IndexMap;
703 use nautilus_core::UnixNanos;
704 use rstest::rstest;
705 use rust_decimal::Decimal;
706
707 use crate::{
708 accounts::{Account, MarginAccount, stubs::*},
709 enums::{AccountType, LiquiditySide, OrderSide, OrderType},
710 events::{AccountState, OrderFilled, account::stubs::*},
711 identifiers::{
712 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
713 VenueOrderId,
714 stubs::{uuid4, *},
715 },
716 instruments::{
717 CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
718 stubs::{binary_option, option_contract_appl, *},
719 },
720 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
721 position::Position,
722 types::{Currency, MarginBalance, Money, Price, Quantity},
723 };
724
725 #[rstest]
726 fn test_display(margin_account: MarginAccount) {
727 assert_eq!(
728 margin_account.to_string(),
729 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
730 );
731 }
732
733 #[rstest]
734 fn test_base_account_properties(
735 margin_account: MarginAccount,
736 margin_account_state: AccountState,
737 ) {
738 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
739 assert_eq!(
740 margin_account.last_event(),
741 Some(margin_account_state.clone())
742 );
743 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
744 assert_eq!(margin_account.event_count(), 1);
745 assert_eq!(
746 margin_account.balance_total(None),
747 Some(Money::from("1525000 USD"))
748 );
749 assert_eq!(
750 margin_account.balance_free(None),
751 Some(Money::from("1500000 USD"))
752 );
753 assert_eq!(
754 margin_account.balance_locked(None),
755 Some(Money::from("25000 USD"))
756 );
757 let mut balances_total_expected = IndexMap::new();
758 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
759 assert_eq!(margin_account.balances_total(), balances_total_expected);
760 let mut balances_free_expected = IndexMap::new();
761 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
762 assert_eq!(margin_account.balances_free(), balances_free_expected);
763 let mut balances_locked_expected = IndexMap::new();
764 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
765 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
766 let margin_balance = margin_account_state.margins[0];
767 let instrument_id = margin_balance
768 .instrument_id
769 .expect("stub margin balance carries a concrete instrument_id");
770 let mut initial_margins_expected = IndexMap::new();
771 initial_margins_expected.insert(instrument_id, margin_balance.initial);
772 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
773 let mut maintenance_margins_expected = IndexMap::new();
774 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
775 assert_eq!(
776 margin_account.maintenance_margins(),
777 maintenance_margins_expected
778 );
779 }
780
781 #[rstest]
782 fn test_set_default_leverage(mut margin_account: MarginAccount) {
783 assert_eq!(margin_account.default_leverage, Decimal::ONE);
784 margin_account.set_default_leverage(Decimal::from(10));
785 assert_eq!(margin_account.default_leverage, Decimal::from(10));
786 }
787
788 #[rstest]
789 fn test_get_leverage_default_leverage(
790 margin_account: MarginAccount,
791 instrument_id_aud_usd_sim: InstrumentId,
792 ) {
793 assert_eq!(
794 margin_account.get_leverage(&instrument_id_aud_usd_sim),
795 Decimal::ONE
796 );
797 }
798
799 #[rstest]
800 fn test_set_leverage(
801 mut margin_account: MarginAccount,
802 instrument_id_aud_usd_sim: InstrumentId,
803 ) {
804 assert_eq!(margin_account.leverages.len(), 0);
805 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
806 assert_eq!(margin_account.leverages.len(), 1);
807 assert_eq!(
808 margin_account.get_leverage(&instrument_id_aud_usd_sim),
809 Decimal::from(10)
810 );
811 }
812
813 #[rstest]
814 fn test_is_unleveraged_with_leverage_returns_false(
815 mut margin_account: MarginAccount,
816 instrument_id_aud_usd_sim: InstrumentId,
817 ) {
818 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
819 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
820 }
821
822 #[rstest]
823 fn test_is_unleveraged_with_no_leverage_returns_true(
824 mut margin_account: MarginAccount,
825 instrument_id_aud_usd_sim: InstrumentId,
826 ) {
827 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
828 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
829 }
830
831 #[rstest]
832 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
833 margin_account: MarginAccount,
834 instrument_id_aud_usd_sim: InstrumentId,
835 ) {
836 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
837 }
838
839 #[rstest]
840 fn test_update_margin_init(
841 mut margin_account: MarginAccount,
842 instrument_id_aud_usd_sim: InstrumentId,
843 ) {
844 assert_eq!(margin_account.margins.len(), 1);
845 let margin = Money::from("10000 USD");
846 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
847 assert_eq!(
848 margin_account.initial_margin(instrument_id_aud_usd_sim),
849 margin
850 );
851 assert_eq!(margin_account.margins.len(), 2);
852 assert_eq!(
853 margin_account
854 .margins
855 .get(&instrument_id_aud_usd_sim)
856 .expect("AUD/USD margin should exist")
857 .initial,
858 margin
859 );
860 }
861
862 #[rstest]
863 fn test_update_margin_maintenance(
864 mut margin_account: MarginAccount,
865 instrument_id_aud_usd_sim: InstrumentId,
866 ) {
867 let margin = Money::from("10000 USD");
868 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
869 assert_eq!(
870 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
871 margin
872 );
873 assert_eq!(margin_account.margins.len(), 2);
874 assert_eq!(
875 margin_account
876 .margins
877 .get(&instrument_id_aud_usd_sim)
878 .expect("AUD/USD margin should exist")
879 .maintenance,
880 margin
881 );
882 }
883
884 #[rstest]
885 fn test_apply_replaces_margin_balances_from_event(
886 mut margin_account: MarginAccount,
887 margin_account_state: AccountState,
888 ) {
889 let old_instrument_id = margin_account_state.margins[0]
890 .instrument_id
891 .expect("stub margin balance carries a concrete instrument_id");
892 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
893 let event = AccountState::new(
894 margin_account_state.account_id,
895 AccountType::Margin,
896 margin_account_state.balances.clone(),
897 vec![MarginBalance::new(
898 Money::from("12500 USD"),
899 Money::from("25000 USD"),
900 Some(new_instrument_id),
901 )],
902 true,
903 uuid4(),
904 1.into(),
905 1.into(),
906 margin_account_state.base_currency,
907 );
908
909 margin_account.apply(event).unwrap();
910
911 assert_eq!(
912 margin_account.initial_margin(new_instrument_id),
913 Money::from("12500 USD")
914 );
915 assert_eq!(
916 margin_account.maintenance_margin(new_instrument_id),
917 Money::from("25000 USD")
918 );
919 assert!(margin_account.margin(&old_instrument_id).is_none());
920 }
921
922 #[rstest]
923 fn test_apply_routes_account_margins_by_currency(
924 mut margin_account: MarginAccount,
925 margin_account_state: AccountState,
926 ) {
927 let usd = Currency::USD();
928 let event = AccountState::new(
929 margin_account_state.account_id,
930 AccountType::Margin,
931 margin_account_state.balances.clone(),
932 vec![MarginBalance::new(
933 Money::from("12500 USD"),
934 Money::from("25000 USD"),
935 None,
936 )],
937 true,
938 uuid4(),
939 1.into(),
940 1.into(),
941 margin_account_state.base_currency,
942 );
943
944 margin_account.apply(event).unwrap();
945
946 assert!(margin_account.margins.is_empty());
947 assert_eq!(margin_account.account_margins.len(), 1);
948 assert_eq!(
949 margin_account.account_initial_margin(&usd),
950 Some(Money::from("12500 USD"))
951 );
952 assert_eq!(
953 margin_account.account_maintenance_margin(&usd),
954 Some(Money::from("25000 USD"))
955 );
956 assert_eq!(
957 margin_account.total_initial_margin(usd),
958 Money::from("12500 USD")
959 );
960 }
961
962 #[rstest]
963 fn test_apply_empty_event_preserves_margin_balances(
964 mut margin_account: MarginAccount,
965 margin_account_state: AccountState,
966 ) {
967 let instrument_id = margin_account_state.margins[0]
968 .instrument_id
969 .expect("stub margin balance carries a concrete instrument_id");
970 let initial_margin = margin_account.initial_margin(instrument_id);
971 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
972
973 let empty_event = AccountState::new(
974 margin_account_state.account_id,
975 AccountType::Margin,
976 vec![],
977 vec![],
978 true,
979 uuid4(),
980 1.into(),
981 1.into(),
982 margin_account_state.base_currency,
983 );
984
985 margin_account.apply(empty_event).unwrap();
986
987 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
988 assert_eq!(
989 margin_account.maintenance_margin(instrument_id),
990 maintenance_margin
991 );
992 assert_eq!(margin_account.event_count(), 2);
993 }
994
995 #[rstest]
996 fn test_calculate_margin_init_with_leverage(
997 mut margin_account: MarginAccount,
998 audusd_sim: CurrencyPair,
999 ) {
1000 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1001 let result = margin_account
1002 .calculate_initial_margin(
1003 &audusd_sim,
1004 Quantity::from(100_000),
1005 Price::from("0.8000"),
1006 None,
1007 )
1008 .unwrap();
1009 assert_eq!(result, Money::from("48.00 USD"));
1010 }
1011
1012 #[rstest]
1013 fn test_calculate_margin_init_with_default_leverage(
1014 mut margin_account: MarginAccount,
1015 audusd_sim: CurrencyPair,
1016 ) {
1017 margin_account.set_default_leverage(Decimal::from(10));
1018 let result = margin_account
1019 .calculate_initial_margin(
1020 &audusd_sim,
1021 Quantity::from(100_000),
1022 Price::from("0.8"),
1023 None,
1024 )
1025 .unwrap();
1026 assert_eq!(result, Money::from("240.00 USD"));
1027 }
1028
1029 #[rstest]
1030 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1031 mut margin_account: MarginAccount,
1032 xbtusd_bitmex: CryptoPerpetual,
1033 ) {
1034 let result_use_quote_inverse_true = margin_account
1035 .calculate_initial_margin(
1036 &xbtusd_bitmex,
1037 Quantity::from(100_000),
1038 Price::from("11493.60"),
1039 Some(false),
1040 )
1041 .unwrap();
1042 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1043 let result_use_quote_inverse_false = margin_account
1044 .calculate_initial_margin(
1045 &xbtusd_bitmex,
1046 Quantity::from(100_000),
1047 Price::from("11493.60"),
1048 Some(true),
1049 )
1050 .unwrap();
1051 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1052 }
1053
1054 #[rstest]
1055 fn test_calculate_margin_maintenance_with_no_leverage(
1056 mut margin_account: MarginAccount,
1057 xbtusd_bitmex: CryptoPerpetual,
1058 ) {
1059 let result = margin_account
1060 .calculate_maintenance_margin(
1061 &xbtusd_bitmex,
1062 Quantity::from(100_000),
1063 Price::from("11493.60"),
1064 None,
1065 )
1066 .unwrap();
1067 assert_eq!(result, Money::from("0.03045173 BTC"));
1068 }
1069
1070 #[rstest]
1071 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1072 mut margin_account: MarginAccount,
1073 audusd_sim: CurrencyPair,
1074 ) {
1075 margin_account.set_default_leverage(Decimal::from(50));
1076 let result = margin_account
1077 .calculate_maintenance_margin(
1078 &audusd_sim,
1079 Quantity::from(1_000_000),
1080 Price::from("1"),
1081 None,
1082 )
1083 .unwrap();
1084 assert_eq!(result, Money::from("600.00 USD"));
1085 }
1086
1087 #[rstest]
1088 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1089 mut margin_account: MarginAccount,
1090 xbtusd_bitmex: CryptoPerpetual,
1091 ) {
1092 margin_account.set_default_leverage(Decimal::from(10));
1093 let result = margin_account
1094 .calculate_maintenance_margin(
1095 &xbtusd_bitmex,
1096 Quantity::from(100_000),
1097 Price::from("100000.00"),
1098 None,
1099 )
1100 .unwrap();
1101 assert_eq!(result, Money::from("0.00035000 BTC"));
1102 }
1103
1104 #[rstest]
1105 fn test_calculate_pnls_github_issue_2657() {
1106 let account_state = margin_account_state();
1108 let account = MarginAccount::new(account_state, false);
1109
1110 let btcusdt = currency_pair_btcusdt();
1112 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1113
1114 let fill1 = OrderFilled::new(
1116 TraderId::from("TRADER-001"),
1117 StrategyId::from("S-001"),
1118 btcusdt_any.id(),
1119 ClientOrderId::from("O-1"),
1120 VenueOrderId::from("V-1"),
1121 AccountId::from("SIM-001"),
1122 TradeId::from("T-1"),
1123 OrderSide::Buy,
1124 OrderType::Market,
1125 Quantity::from("0.001"),
1126 Price::from("50000.00"),
1127 btcusdt_any.quote_currency(),
1128 LiquiditySide::Taker,
1129 uuid4(),
1130 UnixNanos::from(1_000_000_000),
1131 UnixNanos::default(),
1132 false,
1133 Some(PositionId::from("P-GITHUB-2657")),
1134 None,
1135 );
1136
1137 let position = Position::new(&btcusdt_any, fill1);
1138
1139 let fill2 = OrderFilled::new(
1141 TraderId::from("TRADER-001"),
1142 StrategyId::from("S-001"),
1143 btcusdt_any.id(),
1144 ClientOrderId::from("O-2"),
1145 VenueOrderId::from("V-2"),
1146 AccountId::from("SIM-001"),
1147 TradeId::from("T-2"),
1148 OrderSide::Sell,
1149 OrderType::Market,
1150 Quantity::from("0.002"), Price::from("50075.00"),
1152 btcusdt_any.quote_currency(),
1153 LiquiditySide::Taker,
1154 uuid4(),
1155 UnixNanos::from(2_000_000_000),
1156 UnixNanos::default(),
1157 false,
1158 Some(PositionId::from("P-GITHUB-2657")),
1159 None,
1160 );
1161
1162 let pnls = account
1164 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1165 .unwrap();
1166
1167 assert_eq!(pnls.len(), 1);
1169
1170 let expected_pnl = Money::from("0.075 USDT");
1173 assert_eq!(pnls[0], expected_pnl);
1174 }
1175
1176 #[rstest]
1177 #[should_panic(expected = "not positive")]
1178 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1179 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1180 }
1181
1182 #[rstest]
1183 #[should_panic(expected = "not positive")]
1184 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1185 margin_account.set_default_leverage(Decimal::ZERO);
1186 }
1187
1188 #[rstest]
1189 #[should_panic(expected = "not positive")]
1190 fn test_set_leverage_negative_panics(
1191 mut margin_account: MarginAccount,
1192 audusd_sim: CurrencyPair,
1193 ) {
1194 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1195 }
1196
1197 #[rstest]
1198 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1199 use nautilus_core::UnixNanos;
1200
1201 use crate::{
1202 enums::{LiquiditySide, OrderSide, OrderType},
1203 events::OrderFilled,
1204 identifiers::{
1205 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
1206 stubs::uuid4,
1207 },
1208 instruments::InstrumentAny,
1209 position::Position,
1210 types::{Price, Quantity},
1211 };
1212
1213 let account_state = margin_account_state();
1215 let account = MarginAccount::new(account_state, false);
1216
1217 let btcusdt = currency_pair_btcusdt();
1219 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1220
1221 let fill1 = OrderFilled::new(
1223 TraderId::from("TRADER-001"),
1224 StrategyId::from("S-001"),
1225 btcusdt.id,
1226 ClientOrderId::from("O-1"),
1227 VenueOrderId::from("V-1"),
1228 AccountId::from("SIM-001"),
1229 TradeId::from("T-1"),
1230 OrderSide::Buy,
1231 OrderType::Market,
1232 Quantity::from("1.0"),
1233 Price::from("50000.00"),
1234 btcusdt.quote_currency,
1235 LiquiditySide::Taker,
1236 uuid4(),
1237 UnixNanos::from(1_000_000_000),
1238 UnixNanos::default(),
1239 false,
1240 Some(PositionId::from("P-123456")),
1241 None,
1242 );
1243
1244 let position = Position::new(&btcusdt_any, fill1);
1245
1246 let fill2 = OrderFilled::new(
1248 TraderId::from("TRADER-001"),
1249 StrategyId::from("S-001"),
1250 btcusdt.id,
1251 ClientOrderId::from("O-2"),
1252 VenueOrderId::from("V-2"),
1253 AccountId::from("SIM-001"),
1254 TradeId::from("T-2"),
1255 OrderSide::Buy, OrderType::Market,
1257 Quantity::from("0.5"),
1258 Price::from("51000.00"),
1259 btcusdt.quote_currency,
1260 LiquiditySide::Taker,
1261 uuid4(),
1262 UnixNanos::from(2_000_000_000),
1263 UnixNanos::default(),
1264 false,
1265 Some(PositionId::from("P-123456")),
1266 None,
1267 );
1268
1269 let pnls = account
1271 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1272 .unwrap();
1273
1274 assert_eq!(pnls.len(), 0);
1276 }
1277
1278 #[rstest]
1279 fn test_margin_accessor(
1280 mut margin_account: MarginAccount,
1281 instrument_id_aud_usd_sim: InstrumentId,
1282 ) {
1283 let margin_balance = MarginBalance::new(
1284 Money::from("1000 USD"),
1285 Money::from("500 USD"),
1286 Some(instrument_id_aud_usd_sim),
1287 );
1288
1289 margin_account.update_margin(margin_balance);
1290
1291 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1292 assert!(retrieved.is_some());
1293 let retrieved = retrieved.unwrap();
1294 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1295 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1296 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1297 }
1298
1299 #[rstest]
1300 fn test_clear_margin(
1301 mut margin_account: MarginAccount,
1302 instrument_id_aud_usd_sim: InstrumentId,
1303 ) {
1304 let margin_balance = MarginBalance::new(
1305 Money::from("1000 USD"),
1306 Money::from("500 USD"),
1307 Some(instrument_id_aud_usd_sim),
1308 );
1309
1310 margin_account.update_margin(margin_balance);
1311 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1312
1313 margin_account.clear_margin(instrument_id_aud_usd_sim);
1314 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1315 }
1316
1317 #[rstest]
1318 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1319 let usd = Currency::USD();
1320 let margin_balance =
1321 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1322
1323 margin_account.update_margin(margin_balance);
1324
1325 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1326 assert_eq!(
1327 margin_account.account_initial_margin(&usd),
1328 Some(Money::from("200 USD"))
1329 );
1330 assert_eq!(
1331 margin_account.account_maintenance_margin(&usd),
1332 Some(Money::from("100 USD"))
1333 );
1334
1335 margin_account.clear_account_margin(usd);
1336 assert!(margin_account.account_margin(&usd).is_none());
1337 }
1338
1339 #[rstest]
1340 fn test_total_margin_sums_per_instrument_and_account_wide(
1341 mut margin_account: MarginAccount,
1342 instrument_id_aud_usd_sim: InstrumentId,
1343 ) {
1344 let usd = Currency::USD();
1345 let baseline_initial = margin_account.total_initial_margin(usd);
1346 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1347
1348 margin_account.update_margin(MarginBalance::new(
1349 Money::from("100 USD"),
1350 Money::from("50 USD"),
1351 Some(instrument_id_aud_usd_sim),
1352 ));
1353 margin_account.update_margin(MarginBalance::new(
1354 Money::from("200 USD"),
1355 Money::from("150 USD"),
1356 None,
1357 ));
1358
1359 assert_eq!(
1360 margin_account.total_initial_margin(usd).raw,
1361 baseline_initial.raw + Money::from("300 USD").raw,
1362 );
1363 assert_eq!(
1364 margin_account.total_maintenance_margin(usd).raw,
1365 baseline_maintenance.raw + Money::from("200 USD").raw,
1366 );
1367 }
1368
1369 #[rstest]
1370 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1371 let option = option_contract_appl();
1372 let option_any = InstrumentAny::OptionContract(option.clone());
1373
1374 let order = OrderTestBuilder::new(OrderType::Market)
1375 .instrument_id(option.id)
1376 .side(OrderSide::Buy)
1377 .quantity(Quantity::from("10"))
1378 .build();
1379
1380 let fill = TestOrderEventStubs::filled(
1381 &order,
1382 &option_any,
1383 None,
1384 Some(PositionId::new("P-OPT-001")),
1385 Some(Price::from("5.50")),
1386 None,
1387 None,
1388 None,
1389 None,
1390 Some(AccountId::from("SIM-001")),
1391 );
1392
1393 let fill_owned: crate::events::OrderFilled = fill.into();
1394 let pnls = margin_account
1395 .calculate_pnls(&option_any, &fill_owned, None)
1396 .unwrap();
1397
1398 assert_eq!(pnls.len(), 1);
1401 assert_eq!(pnls[0], Money::from("-55 USD"));
1402 }
1403
1404 #[rstest]
1405 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1406 let option = option_contract_appl();
1407 let option_any = InstrumentAny::OptionContract(option.clone());
1408
1409 let order = OrderTestBuilder::new(OrderType::Market)
1410 .instrument_id(option.id)
1411 .side(OrderSide::Sell)
1412 .quantity(Quantity::from("10"))
1413 .build();
1414
1415 let fill = TestOrderEventStubs::filled(
1416 &order,
1417 &option_any,
1418 None,
1419 Some(PositionId::new("P-OPT-002")),
1420 Some(Price::from("5.50")),
1421 None,
1422 None,
1423 None,
1424 None,
1425 Some(AccountId::from("SIM-001")),
1426 );
1427
1428 let fill_owned: crate::events::OrderFilled = fill.into();
1429 let pnls = margin_account
1430 .calculate_pnls(&option_any, &fill_owned, None)
1431 .unwrap();
1432
1433 assert_eq!(pnls.len(), 1);
1436 assert_eq!(pnls[0], Money::from("55 USD"));
1437 }
1438
1439 #[rstest]
1440 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1441 let binary = binary_option();
1442 let binary_any = InstrumentAny::BinaryOption(binary);
1443
1444 let order = OrderTestBuilder::new(OrderType::Market)
1445 .instrument_id(binary_any.id())
1446 .side(OrderSide::Buy)
1447 .quantity(Quantity::from("100"))
1448 .build();
1449
1450 let fill = TestOrderEventStubs::filled(
1451 &order,
1452 &binary_any,
1453 None,
1454 Some(PositionId::new("P-BIN-001")),
1455 Some(Price::from("0.65")),
1456 None,
1457 None,
1458 None,
1459 None,
1460 Some(AccountId::from("SIM-001")),
1461 );
1462
1463 let fill_owned: crate::events::OrderFilled = fill.into();
1464 let pnls = margin_account
1465 .calculate_pnls(&binary_any, &fill_owned, None)
1466 .unwrap();
1467
1468 assert_eq!(pnls.len(), 1);
1469 assert!(pnls[0].as_f64() < 0.0);
1470 }
1471}