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::zero(margin_init.currency),
220 Some(instrument_id),
221 ),
222 );
223 }
224 self.recalculate_balance(margin_init.currency);
225 }
226
227 pub fn clear_initial_margin(&mut self, instrument_id: InstrumentId) {
229 let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
230 return;
231 };
232
233 if margin_balance.maintenance.is_zero() {
234 self.margins.shift_remove(&instrument_id);
235 } else {
236 let mut new_margin_balance = margin_balance;
237 new_margin_balance.initial = Money::zero(margin_balance.currency);
238 self.margins.insert(instrument_id, new_margin_balance);
239 }
240
241 self.recalculate_balance(margin_balance.currency);
242 }
243
244 #[must_use]
250 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
251 let margin_balance = self.margins.get(&instrument_id);
252 assert!(
253 margin_balance.is_some(),
254 "Cannot get margin_init when no margin_balance"
255 );
256 margin_balance.unwrap().initial
257 }
258
259 pub fn update_maintenance_margin(
261 &mut self,
262 instrument_id: InstrumentId,
263 margin_maintenance: Money,
264 ) {
265 let margin_balance = self.margins.get(&instrument_id);
266 if let Some(balance) = margin_balance {
267 let mut new_margin_balance = *balance;
269 new_margin_balance.maintenance = margin_maintenance;
270 self.margins.insert(instrument_id, new_margin_balance);
271 } else {
272 self.margins.insert(
273 instrument_id,
274 MarginBalance::new(
275 Money::zero(margin_maintenance.currency),
276 margin_maintenance,
277 Some(instrument_id),
278 ),
279 );
280 }
281 self.recalculate_balance(margin_maintenance.currency);
282 }
283
284 pub fn clear_maintenance_margin(&mut self, instrument_id: InstrumentId) {
286 let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
287 return;
288 };
289
290 if margin_balance.initial.is_zero() {
291 self.margins.shift_remove(&instrument_id);
292 } else {
293 let mut new_margin_balance = margin_balance;
294 new_margin_balance.maintenance = Money::zero(margin_balance.currency);
295 self.margins.insert(instrument_id, new_margin_balance);
296 }
297
298 self.recalculate_balance(margin_balance.currency);
299 }
300
301 #[must_use]
307 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
308 let margin_balance = self.margins.get(&instrument_id);
309 assert!(
310 margin_balance.is_some(),
311 "Cannot get maintenance_margin when no margin_balance"
312 );
313 margin_balance.unwrap().maintenance
314 }
315
316 #[must_use]
318 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
319 self.margins.get(instrument_id).copied()
320 }
321
322 #[must_use]
324 pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
325 self.account_margins.get(currency).copied()
326 }
327
328 #[must_use]
330 pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
331 self.account_margins.get(currency).map(|m| m.initial)
332 }
333
334 #[must_use]
336 pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
337 self.account_margins.get(currency).map(|m| m.maintenance)
338 }
339
340 #[must_use]
343 pub fn total_initial_margin(&self, currency: Currency) -> Money {
344 let mut raw: MoneyRaw = 0;
345
346 for margin in self.margins.values() {
347 if margin.currency == currency {
348 raw = raw.saturating_add(margin.initial.raw);
349 }
350 }
351
352 for margin in self.account_margins.values() {
353 if margin.currency == currency {
354 raw = raw.saturating_add(margin.initial.raw);
355 }
356 }
357
358 Money::from_raw(raw, currency)
359 }
360
361 #[must_use]
364 pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
365 let mut raw: MoneyRaw = 0;
366
367 for margin in self.margins.values() {
368 if margin.currency == currency {
369 raw = raw.saturating_add(margin.maintenance.raw);
370 }
371 }
372
373 for margin in self.account_margins.values() {
374 if margin.currency == currency {
375 raw = raw.saturating_add(margin.maintenance.raw);
376 }
377 }
378
379 Money::from_raw(raw, currency)
380 }
381
382 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
388 match margin_balance.instrument_id {
389 Some(instrument_id) => {
390 self.margins.insert(instrument_id, margin_balance);
391 }
392 None => {
393 self.account_margins
394 .insert(margin_balance.currency, margin_balance);
395 }
396 }
397 self.recalculate_balance(margin_balance.currency);
398 }
399
400 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
402 if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
403 self.recalculate_balance(margin_balance.currency);
404 }
405 }
406
407 pub fn clear_account_margin(&mut self, currency: Currency) {
409 if self.account_margins.shift_remove(¤cy).is_some() {
410 self.recalculate_balance(currency);
411 }
412 }
413
414 pub fn calculate_initial_margin<T: Instrument>(
423 &mut self,
424 instrument: &T,
425 quantity: Quantity,
426 price: Price,
427 use_quote_for_inverse: Option<bool>,
428 ) -> anyhow::Result<Money> {
429 let leverage = self.get_leverage(&instrument.id());
430 self.margin_model.calculate_initial_margin(
431 instrument,
432 quantity,
433 price,
434 leverage,
435 use_quote_for_inverse,
436 )
437 }
438
439 pub fn calculate_maintenance_margin<T: Instrument>(
447 &mut self,
448 instrument: &T,
449 quantity: Quantity,
450 price: Price,
451 use_quote_for_inverse: Option<bool>,
452 ) -> anyhow::Result<Money> {
453 let leverage = self.get_leverage(&instrument.id());
454 self.margin_model.calculate_maintenance_margin(
455 instrument,
456 quantity,
457 price,
458 leverage,
459 use_quote_for_inverse,
460 )
461 }
462
463 pub fn recalculate_balance(&mut self, currency: Currency) {
470 let current_balance = if let Some(balance) = self.balances.get(¤cy) {
471 *balance
472 } else {
473 let zero = Money::from_raw(0, currency);
476 AccountBalance::new(zero, zero, zero)
477 };
478
479 let mut total_margin: MoneyRaw = 0;
480
481 let accumulate = |raw: MoneyRaw, margin: &MarginBalance| -> MoneyRaw {
482 raw.checked_add(margin.initial.raw)
483 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
484 .unwrap_or_else(|| {
485 panic!(
486 "Margin calculation overflow for currency {}: total would exceed maximum",
487 currency.code
488 )
489 })
490 };
491
492 for margin in self.margins.values() {
493 if margin.currency == currency {
494 total_margin = accumulate(total_margin, margin);
495 }
496 }
497
498 for margin in self.account_margins.values() {
499 if margin.currency == currency {
500 total_margin = accumulate(total_margin, margin);
501 }
502 }
503
504 let total_free = if total_margin > current_balance.total.raw {
508 total_margin = current_balance.total.raw.max(0);
509 current_balance.total.raw - total_margin
510 } else {
511 current_balance.total.raw - total_margin
512 };
513
514 let new_balance = AccountBalance::new(
515 current_balance.total,
516 Money::from_raw(total_margin, currency),
517 Money::from_raw(total_free, currency),
518 );
519 self.balances.insert(currency, new_balance);
520 }
521}
522
523impl Deref for MarginAccount {
524 type Target = BaseAccount;
525
526 fn deref(&self) -> &Self::Target {
527 &self.base
528 }
529}
530
531impl DerefMut for MarginAccount {
532 fn deref_mut(&mut self) -> &mut Self::Target {
533 &mut self.base
534 }
535}
536
537impl Account for MarginAccount {
538 fn id(&self) -> AccountId {
539 self.id
540 }
541
542 fn account_type(&self) -> AccountType {
543 self.account_type
544 }
545
546 fn base_currency(&self) -> Option<Currency> {
547 self.base_currency
548 }
549
550 fn is_cash_account(&self) -> bool {
551 self.account_type == AccountType::Cash
552 }
553
554 fn is_margin_account(&self) -> bool {
555 self.account_type == AccountType::Margin
556 }
557
558 fn calculated_account_state(&self) -> bool {
559 self.calculate_account_state
560 }
561
562 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
563 self.base_balance_total(currency)
564 }
565
566 fn balances_total(&self) -> IndexMap<Currency, Money> {
567 self.base_balances_total()
568 }
569
570 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
571 self.base_balance_free(currency)
572 }
573
574 fn balances_free(&self) -> IndexMap<Currency, Money> {
575 self.base_balances_free()
576 }
577
578 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
579 self.base_balance_locked(currency)
580 }
581
582 fn balances_locked(&self) -> IndexMap<Currency, Money> {
583 self.base_balances_locked()
584 }
585
586 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
587 self.base_balance(currency)
588 }
589
590 fn last_event(&self) -> Option<AccountState> {
591 self.base_last_event()
592 }
593
594 fn events(&self) -> Vec<AccountState> {
595 self.events.clone()
596 }
597
598 fn event_count(&self) -> usize {
599 self.events.len()
600 }
601
602 fn currencies(&self) -> Vec<Currency> {
603 self.balances.keys().copied().collect()
604 }
605
606 fn starting_balances(&self) -> IndexMap<Currency, Money> {
607 self.balances_starting.clone()
608 }
609
610 fn balances(&self) -> IndexMap<Currency, AccountBalance> {
611 self.balances.clone()
612 }
613
614 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
615 let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
616 let (per_instrument, per_currency) = split_event_margins(&event);
617 self.base_apply(event);
618
619 if !skip_margin_routing {
620 self.margins = per_instrument;
621 self.account_margins = per_currency;
622 }
623 Ok(())
624 }
625
626 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
627 self.base.base_purge_account_events(ts_now, lookback_secs);
628 }
629
630 fn calculate_balance_locked(
631 &mut self,
632 instrument: &InstrumentAny,
633 side: OrderSide,
634 quantity: Quantity,
635 price: Price,
636 use_quote_for_inverse: Option<bool>,
637 ) -> anyhow::Result<Money> {
638 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
639 }
640
641 fn calculate_pnls(
642 &self,
643 instrument: &InstrumentAny,
644 fill: &OrderFilled,
645 position: Option<Position>,
646 ) -> anyhow::Result<Vec<Money>> {
647 let mut pnls: Vec<Money> = Vec::new();
648
649 let instrument_class = instrument.instrument_class();
651
652 if matches!(
653 instrument_class,
654 InstrumentClass::Option
655 | InstrumentClass::OptionSpread
656 | InstrumentClass::BinaryOption
657 | InstrumentClass::Warrant
658 ) {
659 let notional = instrument.calculate_notional_value(fill.last_qty, fill.last_px, None);
660 let pnl = if fill.order_side == OrderSide::Buy {
661 Money::from_raw(-notional.raw, notional.currency)
662 } else {
663 notional
664 };
665 pnls.push(pnl);
666 return Ok(pnls);
667 }
668
669 if let Some(ref pos) = position
671 && pos.quantity.is_positive()
672 && pos.entry != fill.order_side
673 {
674 let pnl_quantity = Quantity::from_raw(
677 fill.last_qty.raw.min(pos.quantity.raw),
678 fill.last_qty.precision,
679 );
680 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
681 pnls.push(pnl);
682 }
683
684 Ok(pnls)
685 }
686
687 fn calculate_commission(
688 &self,
689 instrument: &InstrumentAny,
690 last_qty: Quantity,
691 last_px: Price,
692 liquidity_side: LiquiditySide,
693 use_quote_for_inverse: Option<bool>,
694 ) -> anyhow::Result<Money> {
695 self.base_calculate_commission(
696 instrument,
697 last_qty,
698 last_px,
699 liquidity_side,
700 use_quote_for_inverse,
701 )
702 }
703}
704
705impl PartialEq for MarginAccount {
706 fn eq(&self, other: &Self) -> bool {
707 self.id == other.id
708 }
709}
710
711impl Eq for MarginAccount {}
712
713impl Display for MarginAccount {
714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 write!(
716 f,
717 "MarginAccount(id={}, type={}, base={})",
718 self.id,
719 self.account_type,
720 self.base_currency.map_or_else(
721 || "None".to_string(),
722 |base_currency| format!("{}", base_currency.code)
723 ),
724 )
725 }
726}
727
728impl Hash for MarginAccount {
729 fn hash<H: Hasher>(&self, state: &mut H) {
730 self.id.hash(state);
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use indexmap::IndexMap;
737 use nautilus_core::UnixNanos;
738 use rstest::rstest;
739 use rust_decimal::Decimal;
740
741 use crate::{
742 accounts::{Account, MarginAccount, stubs::*},
743 enums::{AccountType, OrderSide, OrderType},
744 events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
745 identifiers::{
746 AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
747 stubs::{uuid4, *},
748 },
749 instruments::{
750 CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
751 stubs::{binary_option, option_contract_appl, *},
752 },
753 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
754 position::Position,
755 types::{Currency, MarginBalance, Money, Price, Quantity},
756 };
757
758 #[rstest]
759 fn test_display(margin_account: MarginAccount) {
760 assert_eq!(
761 margin_account.to_string(),
762 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
763 );
764 }
765
766 #[rstest]
767 fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
768 assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
769 assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
770 }
771
772 #[rstest]
773 fn test_base_account_properties(
774 margin_account: MarginAccount,
775 margin_account_state: AccountState,
776 ) {
777 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
778 assert_eq!(
779 margin_account.last_event(),
780 Some(margin_account_state.clone())
781 );
782 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
783 assert_eq!(margin_account.event_count(), 1);
784 assert_eq!(
785 margin_account.balance_total(None),
786 Some(Money::from("1525000 USD"))
787 );
788 assert_eq!(
789 margin_account.balance_free(None),
790 Some(Money::from("1500000 USD"))
791 );
792 assert_eq!(
793 margin_account.balance_locked(None),
794 Some(Money::from("25000 USD"))
795 );
796 let mut balances_total_expected = IndexMap::new();
797 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
798 assert_eq!(margin_account.balances_total(), balances_total_expected);
799 let mut balances_free_expected = IndexMap::new();
800 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
801 assert_eq!(margin_account.balances_free(), balances_free_expected);
802 let mut balances_locked_expected = IndexMap::new();
803 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
804 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
805 let margin_balance = margin_account_state.margins[0];
806 let instrument_id = margin_balance
807 .instrument_id
808 .expect("stub margin balance carries a concrete instrument_id");
809 let mut initial_margins_expected = IndexMap::new();
810 initial_margins_expected.insert(instrument_id, margin_balance.initial);
811 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
812 let mut maintenance_margins_expected = IndexMap::new();
813 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
814 assert_eq!(
815 margin_account.maintenance_margins(),
816 maintenance_margins_expected
817 );
818 }
819
820 #[rstest]
821 fn test_set_default_leverage(mut margin_account: MarginAccount) {
822 assert_eq!(margin_account.default_leverage, Decimal::ONE);
823 margin_account.set_default_leverage(Decimal::from(10));
824 assert_eq!(margin_account.default_leverage, Decimal::from(10));
825 }
826
827 #[rstest]
828 fn test_get_leverage_default_leverage(
829 margin_account: MarginAccount,
830 instrument_id_aud_usd_sim: InstrumentId,
831 ) {
832 assert_eq!(
833 margin_account.get_leverage(&instrument_id_aud_usd_sim),
834 Decimal::ONE
835 );
836 }
837
838 #[rstest]
839 fn test_set_leverage(
840 mut margin_account: MarginAccount,
841 instrument_id_aud_usd_sim: InstrumentId,
842 ) {
843 assert_eq!(margin_account.leverages.len(), 0);
844 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
845 assert_eq!(margin_account.leverages.len(), 1);
846 assert_eq!(
847 margin_account.get_leverage(&instrument_id_aud_usd_sim),
848 Decimal::from(10)
849 );
850 }
851
852 #[rstest]
853 fn test_is_unleveraged_with_leverage_returns_false(
854 mut margin_account: MarginAccount,
855 instrument_id_aud_usd_sim: InstrumentId,
856 ) {
857 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
858 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
859 }
860
861 #[rstest]
862 fn test_is_unleveraged_with_no_leverage_returns_true(
863 mut margin_account: MarginAccount,
864 instrument_id_aud_usd_sim: InstrumentId,
865 ) {
866 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
867 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
868 }
869
870 #[rstest]
871 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
872 margin_account: MarginAccount,
873 instrument_id_aud_usd_sim: InstrumentId,
874 ) {
875 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
876 }
877
878 #[rstest]
879 fn test_update_margin_init(
880 mut margin_account: MarginAccount,
881 instrument_id_aud_usd_sim: InstrumentId,
882 ) {
883 assert_eq!(margin_account.margins.len(), 1);
884 let margin = Money::from("10000 USD");
885 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
886 assert_eq!(
887 margin_account.initial_margin(instrument_id_aud_usd_sim),
888 margin
889 );
890 assert_eq!(margin_account.margins.len(), 2);
891 assert_eq!(
892 margin_account
893 .margins
894 .get(&instrument_id_aud_usd_sim)
895 .expect("AUD/USD margin should exist")
896 .initial,
897 margin
898 );
899 assert_eq!(
900 margin_account
901 .margins
902 .get(&instrument_id_aud_usd_sim)
903 .expect("AUD/USD margin should exist")
904 .maintenance,
905 Money::zero(margin.currency)
906 );
907 }
908
909 #[rstest]
910 fn test_update_margin_maintenance(
911 mut margin_account: MarginAccount,
912 instrument_id_aud_usd_sim: InstrumentId,
913 ) {
914 let margin = Money::from("10000 USD");
915 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
916 assert_eq!(
917 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
918 margin
919 );
920 assert_eq!(margin_account.margins.len(), 2);
921 assert_eq!(
922 margin_account
923 .margins
924 .get(&instrument_id_aud_usd_sim)
925 .expect("AUD/USD margin should exist")
926 .maintenance,
927 margin
928 );
929 assert_eq!(
930 margin_account
931 .margins
932 .get(&instrument_id_aud_usd_sim)
933 .expect("AUD/USD margin should exist")
934 .initial,
935 Money::zero(margin.currency)
936 );
937 }
938
939 #[rstest]
940 fn test_clear_initial_margin_preserves_maintenance(
941 mut margin_account: MarginAccount,
942 instrument_id_aud_usd_sim: InstrumentId,
943 ) {
944 margin_account.update_margin(MarginBalance::new(
945 Money::from("1000 USD"),
946 Money::from("500 USD"),
947 Some(instrument_id_aud_usd_sim),
948 ));
949
950 margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
951
952 let margin = margin_account
953 .margin(&instrument_id_aud_usd_sim)
954 .expect("margin should retain non-zero maintenance");
955 assert_eq!(margin.initial, Money::from("0 USD"));
956 assert_eq!(margin.maintenance, Money::from("500 USD"));
957 }
958
959 #[rstest]
960 fn test_clear_maintenance_margin_removes_empty_entry(
961 mut margin_account: MarginAccount,
962 instrument_id_aud_usd_sim: InstrumentId,
963 ) {
964 margin_account.update_margin(MarginBalance::new(
965 Money::from("0 USD"),
966 Money::from("500 USD"),
967 Some(instrument_id_aud_usd_sim),
968 ));
969
970 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
971
972 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
973 }
974
975 #[rstest]
976 fn test_clear_maintenance_margin_preserves_initial(
977 mut margin_account: MarginAccount,
978 instrument_id_aud_usd_sim: InstrumentId,
979 ) {
980 margin_account.update_margin(MarginBalance::new(
981 Money::from("1000 USD"),
982 Money::from("500 USD"),
983 Some(instrument_id_aud_usd_sim),
984 ));
985
986 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
987
988 let margin = margin_account
989 .margin(&instrument_id_aud_usd_sim)
990 .expect("margin should retain non-zero initial");
991 assert_eq!(margin.initial, Money::from("1000 USD"));
992 assert_eq!(margin.maintenance, Money::from("0 USD"));
993 }
994
995 #[rstest]
996 fn test_apply_replaces_margin_balances_from_event(
997 mut margin_account: MarginAccount,
998 margin_account_state: AccountState,
999 ) {
1000 let old_instrument_id = margin_account_state.margins[0]
1001 .instrument_id
1002 .expect("stub margin balance carries a concrete instrument_id");
1003 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1004 let event = AccountState::new(
1005 margin_account_state.account_id,
1006 AccountType::Margin,
1007 margin_account_state.balances.clone(),
1008 vec![MarginBalance::new(
1009 Money::from("12500 USD"),
1010 Money::from("25000 USD"),
1011 Some(new_instrument_id),
1012 )],
1013 true,
1014 uuid4(),
1015 1.into(),
1016 1.into(),
1017 margin_account_state.base_currency,
1018 );
1019
1020 margin_account.apply(event).unwrap();
1021
1022 assert_eq!(
1023 margin_account.initial_margin(new_instrument_id),
1024 Money::from("12500 USD")
1025 );
1026 assert_eq!(
1027 margin_account.maintenance_margin(new_instrument_id),
1028 Money::from("25000 USD")
1029 );
1030 assert!(margin_account.margin(&old_instrument_id).is_none());
1031 }
1032
1033 #[rstest]
1034 fn test_apply_routes_account_margins_by_currency(
1035 mut margin_account: MarginAccount,
1036 margin_account_state: AccountState,
1037 ) {
1038 let usd = Currency::USD();
1039 let event = AccountState::new(
1040 margin_account_state.account_id,
1041 AccountType::Margin,
1042 margin_account_state.balances.clone(),
1043 vec![MarginBalance::new(
1044 Money::from("12500 USD"),
1045 Money::from("25000 USD"),
1046 None,
1047 )],
1048 true,
1049 uuid4(),
1050 1.into(),
1051 1.into(),
1052 margin_account_state.base_currency,
1053 );
1054
1055 margin_account.apply(event).unwrap();
1056
1057 assert!(margin_account.margins.is_empty());
1058 assert_eq!(margin_account.account_margins.len(), 1);
1059 assert_eq!(
1060 margin_account.account_initial_margin(&usd),
1061 Some(Money::from("12500 USD"))
1062 );
1063 assert_eq!(
1064 margin_account.account_maintenance_margin(&usd),
1065 Some(Money::from("25000 USD"))
1066 );
1067 assert_eq!(
1068 margin_account.total_initial_margin(usd),
1069 Money::from("12500 USD")
1070 );
1071 }
1072
1073 #[rstest]
1074 fn test_apply_empty_event_preserves_margin_balances(
1075 mut margin_account: MarginAccount,
1076 margin_account_state: AccountState,
1077 ) {
1078 let instrument_id = margin_account_state.margins[0]
1079 .instrument_id
1080 .expect("stub margin balance carries a concrete instrument_id");
1081 let initial_margin = margin_account.initial_margin(instrument_id);
1082 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1083
1084 let empty_event = AccountState::new(
1085 margin_account_state.account_id,
1086 AccountType::Margin,
1087 vec![],
1088 vec![],
1089 true,
1090 uuid4(),
1091 1.into(),
1092 1.into(),
1093 margin_account_state.base_currency,
1094 );
1095
1096 margin_account.apply(empty_event).unwrap();
1097
1098 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1099 assert_eq!(
1100 margin_account.maintenance_margin(instrument_id),
1101 maintenance_margin
1102 );
1103 assert_eq!(margin_account.event_count(), 2);
1104 }
1105
1106 #[rstest]
1107 fn test_calculate_margin_init_with_leverage(
1108 mut margin_account: MarginAccount,
1109 audusd_sim: CurrencyPair,
1110 ) {
1111 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1112 let result = margin_account
1113 .calculate_initial_margin(
1114 &audusd_sim,
1115 Quantity::from(100_000),
1116 Price::from("0.8000"),
1117 None,
1118 )
1119 .unwrap();
1120 assert_eq!(result, Money::from("48.00 USD"));
1121 }
1122
1123 #[rstest]
1124 fn test_calculate_margin_init_with_default_leverage(
1125 mut margin_account: MarginAccount,
1126 audusd_sim: CurrencyPair,
1127 ) {
1128 margin_account.set_default_leverage(Decimal::from(10));
1129 let result = margin_account
1130 .calculate_initial_margin(
1131 &audusd_sim,
1132 Quantity::from(100_000),
1133 Price::from("0.8"),
1134 None,
1135 )
1136 .unwrap();
1137 assert_eq!(result, Money::from("240.00 USD"));
1138 }
1139
1140 #[rstest]
1141 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1142 mut margin_account: MarginAccount,
1143 xbtusd_bitmex: CryptoPerpetual,
1144 ) {
1145 let result_use_quote_inverse_true = margin_account
1146 .calculate_initial_margin(
1147 &xbtusd_bitmex,
1148 Quantity::from(100_000),
1149 Price::from("11493.60"),
1150 Some(false),
1151 )
1152 .unwrap();
1153 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1154 let result_use_quote_inverse_false = margin_account
1155 .calculate_initial_margin(
1156 &xbtusd_bitmex,
1157 Quantity::from(100_000),
1158 Price::from("11493.60"),
1159 Some(true),
1160 )
1161 .unwrap();
1162 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1163 }
1164
1165 #[rstest]
1166 fn test_calculate_margin_maintenance_with_no_leverage(
1167 mut margin_account: MarginAccount,
1168 xbtusd_bitmex: CryptoPerpetual,
1169 ) {
1170 let result = margin_account
1171 .calculate_maintenance_margin(
1172 &xbtusd_bitmex,
1173 Quantity::from(100_000),
1174 Price::from("11493.60"),
1175 None,
1176 )
1177 .unwrap();
1178 assert_eq!(result, Money::from("0.03045173 BTC"));
1179 }
1180
1181 #[rstest]
1182 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1183 mut margin_account: MarginAccount,
1184 audusd_sim: CurrencyPair,
1185 ) {
1186 margin_account.set_default_leverage(Decimal::from(50));
1187 let result = margin_account
1188 .calculate_maintenance_margin(
1189 &audusd_sim,
1190 Quantity::from(1_000_000),
1191 Price::from("1"),
1192 None,
1193 )
1194 .unwrap();
1195 assert_eq!(result, Money::from("600.00 USD"));
1196 }
1197
1198 #[rstest]
1199 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1200 mut margin_account: MarginAccount,
1201 xbtusd_bitmex: CryptoPerpetual,
1202 ) {
1203 margin_account.set_default_leverage(Decimal::from(10));
1204 let result = margin_account
1205 .calculate_maintenance_margin(
1206 &xbtusd_bitmex,
1207 Quantity::from(100_000),
1208 Price::from("100000.00"),
1209 None,
1210 )
1211 .unwrap();
1212 assert_eq!(result, Money::from("0.00035000 BTC"));
1213 }
1214
1215 #[rstest]
1216 fn test_calculate_pnls_github_issue_2657() {
1217 let account_state = margin_account_state();
1219 let account = MarginAccount::new(account_state, false);
1220
1221 let btcusdt = currency_pair_btcusdt();
1223 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1224
1225 let fill1 = OrderFilledSpec::builder()
1227 .instrument_id(btcusdt_any.id())
1228 .client_order_id(ClientOrderId::from("O-1"))
1229 .venue_order_id(VenueOrderId::from("V-1"))
1230 .trade_id(TradeId::from("T-1"))
1231 .last_qty(Quantity::from("0.001"))
1232 .last_px(Price::from("50000.00"))
1233 .currency(btcusdt_any.quote_currency())
1234 .ts_event(UnixNanos::from(1_000_000_000))
1235 .position_id(PositionId::from("P-GITHUB-2657"))
1236 .build();
1237
1238 let position = Position::new(&btcusdt_any, fill1);
1239
1240 let fill2 = OrderFilledSpec::builder()
1242 .instrument_id(btcusdt_any.id())
1243 .client_order_id(ClientOrderId::from("O-2"))
1244 .venue_order_id(VenueOrderId::from("V-2"))
1245 .trade_id(TradeId::from("T-2"))
1246 .order_side(OrderSide::Sell)
1247 .last_qty(Quantity::from("0.002")) .last_px(Price::from("50075.00"))
1249 .currency(btcusdt_any.quote_currency())
1250 .ts_event(UnixNanos::from(2_000_000_000))
1251 .position_id(PositionId::from("P-GITHUB-2657"))
1252 .build();
1253
1254 let pnls = account
1256 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1257 .unwrap();
1258
1259 assert_eq!(pnls.len(), 1);
1261
1262 let expected_pnl = Money::from("0.075 USDT");
1265 assert_eq!(pnls[0], expected_pnl);
1266 }
1267
1268 #[rstest]
1269 #[should_panic(expected = "not positive")]
1270 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1271 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1272 }
1273
1274 #[rstest]
1275 #[should_panic(expected = "not positive")]
1276 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1277 margin_account.set_default_leverage(Decimal::ZERO);
1278 }
1279
1280 #[rstest]
1281 #[should_panic(expected = "not positive")]
1282 fn test_set_leverage_negative_panics(
1283 mut margin_account: MarginAccount,
1284 audusd_sim: CurrencyPair,
1285 ) {
1286 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1287 }
1288
1289 #[rstest]
1290 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1291 use nautilus_core::UnixNanos;
1292
1293 use crate::{
1294 events::order::spec::OrderFilledSpec,
1295 identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1296 instruments::InstrumentAny,
1297 position::Position,
1298 types::{Price, Quantity},
1299 };
1300
1301 let account_state = margin_account_state();
1303 let account = MarginAccount::new(account_state, false);
1304
1305 let btcusdt = currency_pair_btcusdt();
1307 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1308
1309 let fill1 = OrderFilledSpec::builder()
1311 .instrument_id(btcusdt.id)
1312 .client_order_id(ClientOrderId::from("O-1"))
1313 .venue_order_id(VenueOrderId::from("V-1"))
1314 .trade_id(TradeId::from("T-1"))
1315 .last_qty(Quantity::from("1.0"))
1316 .last_px(Price::from("50000.00"))
1317 .currency(btcusdt.quote_currency)
1318 .ts_event(UnixNanos::from(1_000_000_000))
1319 .position_id(PositionId::from("P-123456"))
1320 .build();
1321
1322 let position = Position::new(&btcusdt_any, fill1);
1323
1324 let fill2 = OrderFilledSpec::builder()
1326 .instrument_id(btcusdt.id)
1327 .client_order_id(ClientOrderId::from("O-2"))
1328 .venue_order_id(VenueOrderId::from("V-2"))
1329 .trade_id(TradeId::from("T-2"))
1330 .last_qty(Quantity::from("0.5"))
1331 .last_px(Price::from("51000.00"))
1332 .currency(btcusdt.quote_currency)
1333 .ts_event(UnixNanos::from(2_000_000_000))
1334 .position_id(PositionId::from("P-123456"))
1335 .build();
1336
1337 let pnls = account
1339 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1340 .unwrap();
1341
1342 assert_eq!(pnls.len(), 0);
1344 }
1345
1346 #[rstest]
1347 fn test_margin_accessor(
1348 mut margin_account: MarginAccount,
1349 instrument_id_aud_usd_sim: InstrumentId,
1350 ) {
1351 let margin_balance = MarginBalance::new(
1352 Money::from("1000 USD"),
1353 Money::from("500 USD"),
1354 Some(instrument_id_aud_usd_sim),
1355 );
1356
1357 margin_account.update_margin(margin_balance);
1358
1359 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1360 assert!(retrieved.is_some());
1361 let retrieved = retrieved.unwrap();
1362 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1363 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1364 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1365 }
1366
1367 #[rstest]
1368 fn test_clear_margin(
1369 mut margin_account: MarginAccount,
1370 instrument_id_aud_usd_sim: InstrumentId,
1371 ) {
1372 let margin_balance = MarginBalance::new(
1373 Money::from("1000 USD"),
1374 Money::from("500 USD"),
1375 Some(instrument_id_aud_usd_sim),
1376 );
1377
1378 margin_account.update_margin(margin_balance);
1379 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1380
1381 margin_account.clear_margin(instrument_id_aud_usd_sim);
1382 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1383 }
1384
1385 #[rstest]
1386 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1387 let usd = Currency::USD();
1388 let margin_balance =
1389 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1390
1391 margin_account.update_margin(margin_balance);
1392
1393 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1394 assert_eq!(
1395 margin_account.account_initial_margin(&usd),
1396 Some(Money::from("200 USD"))
1397 );
1398 assert_eq!(
1399 margin_account.account_maintenance_margin(&usd),
1400 Some(Money::from("100 USD"))
1401 );
1402
1403 margin_account.clear_account_margin(usd);
1404 assert!(margin_account.account_margin(&usd).is_none());
1405 }
1406
1407 #[rstest]
1408 fn test_total_margin_sums_per_instrument_and_account_wide(
1409 mut margin_account: MarginAccount,
1410 instrument_id_aud_usd_sim: InstrumentId,
1411 ) {
1412 let usd = Currency::USD();
1413 let baseline_initial = margin_account.total_initial_margin(usd);
1414 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1415
1416 margin_account.update_margin(MarginBalance::new(
1417 Money::from("100 USD"),
1418 Money::from("50 USD"),
1419 Some(instrument_id_aud_usd_sim),
1420 ));
1421 margin_account.update_margin(MarginBalance::new(
1422 Money::from("200 USD"),
1423 Money::from("150 USD"),
1424 None,
1425 ));
1426
1427 assert_eq!(
1428 margin_account.total_initial_margin(usd).raw,
1429 baseline_initial.raw + Money::from("300 USD").raw,
1430 );
1431 assert_eq!(
1432 margin_account.total_maintenance_margin(usd).raw,
1433 baseline_maintenance.raw + Money::from("200 USD").raw,
1434 );
1435 }
1436
1437 #[rstest]
1438 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1439 let option = option_contract_appl();
1440 let option_any = InstrumentAny::OptionContract(option.clone());
1441
1442 let order = OrderTestBuilder::new(OrderType::Market)
1443 .instrument_id(option.id)
1444 .side(OrderSide::Buy)
1445 .quantity(Quantity::from("10"))
1446 .build();
1447
1448 let fill = TestOrderEventStubs::filled(
1449 &order,
1450 &option_any,
1451 None,
1452 Some(PositionId::new("P-OPT-001")),
1453 Some(Price::from("5.50")),
1454 None,
1455 None,
1456 None,
1457 None,
1458 Some(AccountId::from("SIM-001")),
1459 );
1460
1461 let fill_owned: crate::events::OrderFilled = fill.into();
1462 let pnls = margin_account
1463 .calculate_pnls(&option_any, &fill_owned, None)
1464 .unwrap();
1465
1466 assert_eq!(pnls.len(), 1);
1469 assert_eq!(pnls[0], Money::from("-55 USD"));
1470 }
1471
1472 #[rstest]
1473 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1474 let option = option_contract_appl();
1475 let option_any = InstrumentAny::OptionContract(option.clone());
1476
1477 let order = OrderTestBuilder::new(OrderType::Market)
1478 .instrument_id(option.id)
1479 .side(OrderSide::Sell)
1480 .quantity(Quantity::from("10"))
1481 .build();
1482
1483 let fill = TestOrderEventStubs::filled(
1484 &order,
1485 &option_any,
1486 None,
1487 Some(PositionId::new("P-OPT-002")),
1488 Some(Price::from("5.50")),
1489 None,
1490 None,
1491 None,
1492 None,
1493 Some(AccountId::from("SIM-001")),
1494 );
1495
1496 let fill_owned: crate::events::OrderFilled = fill.into();
1497 let pnls = margin_account
1498 .calculate_pnls(&option_any, &fill_owned, None)
1499 .unwrap();
1500
1501 assert_eq!(pnls.len(), 1);
1504 assert_eq!(pnls[0], Money::from("55 USD"));
1505 }
1506
1507 #[rstest]
1508 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1509 let binary = binary_option();
1510 let binary_any = InstrumentAny::BinaryOption(binary);
1511
1512 let order = OrderTestBuilder::new(OrderType::Market)
1513 .instrument_id(binary_any.id())
1514 .side(OrderSide::Buy)
1515 .quantity(Quantity::from("100"))
1516 .build();
1517
1518 let fill = TestOrderEventStubs::filled(
1519 &order,
1520 &binary_any,
1521 None,
1522 Some(PositionId::new("P-BIN-001")),
1523 Some(Price::from("0.65")),
1524 None,
1525 None,
1526 None,
1527 None,
1528 Some(AccountId::from("SIM-001")),
1529 );
1530
1531 let fill_owned: crate::events::OrderFilled = fill.into();
1532 let pnls = margin_account
1533 .calculate_pnls(&binary_any, &fill_owned, None)
1534 .unwrap();
1535
1536 assert_eq!(pnls.len(), 1);
1537 assert!(pnls[0].as_f64() < 0.0);
1538 }
1539}