1#![allow(dead_code)]
20
21use std::{
22 collections::HashMap,
23 fmt::Display,
24 hash::{Hash, Hasher},
25 ops::{Deref, DerefMut},
26};
27
28use rust_decimal::Decimal;
29use serde::{Deserialize, Serialize};
30
31use crate::{
32 accounts::{Account, base::BaseAccount},
33 enums::{AccountType, LiquiditySide, OrderSide},
34 events::{AccountState, OrderFilled},
35 identifiers::{AccountId, InstrumentId},
36 instruments::{Instrument, InstrumentAny},
37 position::Position,
38 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity, money::MoneyRaw},
39};
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
45)]
46pub struct MarginAccount {
47 pub base: BaseAccount,
48 pub leverages: HashMap<InstrumentId, Decimal>,
49 pub margins: HashMap<InstrumentId, MarginBalance>,
50 pub default_leverage: Decimal,
51}
52
53impl MarginAccount {
54 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
56 Self {
57 base: BaseAccount::new(event, calculate_account_state),
58 leverages: HashMap::new(),
59 margins: HashMap::new(),
60 default_leverage: Decimal::ONE,
61 }
62 }
63
64 pub fn set_default_leverage(&mut self, leverage: Decimal) {
65 self.default_leverage = leverage;
66 }
67
68 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
69 self.leverages.insert(instrument_id, leverage);
70 }
71
72 #[must_use]
73 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
74 *self
75 .leverages
76 .get(instrument_id)
77 .unwrap_or(&self.default_leverage)
78 }
79
80 #[must_use]
81 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
82 self.get_leverage(&instrument_id) == Decimal::ONE
83 }
84
85 #[must_use]
86 pub fn is_cash_account(&self) -> bool {
87 self.account_type == AccountType::Cash
88 }
89 #[must_use]
90 pub fn is_margin_account(&self) -> bool {
91 self.account_type == AccountType::Margin
92 }
93
94 #[must_use]
95 pub fn initial_margins(&self) -> HashMap<InstrumentId, Money> {
96 let mut initial_margins: HashMap<InstrumentId, Money> = HashMap::new();
97 self.margins.values().for_each(|margin_balance| {
98 initial_margins.insert(margin_balance.instrument_id, margin_balance.initial);
99 });
100 initial_margins
101 }
102
103 #[must_use]
104 pub fn maintenance_margins(&self) -> HashMap<InstrumentId, Money> {
105 let mut maintenance_margins: HashMap<InstrumentId, Money> = HashMap::new();
106 self.margins.values().for_each(|margin_balance| {
107 maintenance_margins.insert(margin_balance.instrument_id, margin_balance.maintenance);
108 });
109 maintenance_margins
110 }
111
112 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
118 let margin_balance = self.margins.get(&instrument_id);
119 if let Some(balance) = margin_balance {
120 let mut new_margin_balance = *balance;
122 new_margin_balance.initial = margin_init;
123 self.margins.insert(instrument_id, new_margin_balance);
124 } else {
125 self.margins.insert(
126 instrument_id,
127 MarginBalance::new(
128 margin_init,
129 Money::new(0.0, margin_init.currency),
130 instrument_id,
131 ),
132 );
133 }
134 self.recalculate_balance(margin_init.currency);
135 }
136
137 #[must_use]
143 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
144 let margin_balance = self.margins.get(&instrument_id);
145 assert!(
146 margin_balance.is_some(),
147 "Cannot get margin_init when no margin_balance"
148 );
149 margin_balance.unwrap().initial
150 }
151
152 pub fn update_maintenance_margin(
158 &mut self,
159 instrument_id: InstrumentId,
160 margin_maintenance: Money,
161 ) {
162 let margin_balance = self.margins.get(&instrument_id);
163 if let Some(balance) = margin_balance {
164 let mut new_margin_balance = *balance;
166 new_margin_balance.maintenance = margin_maintenance;
167 self.margins.insert(instrument_id, new_margin_balance);
168 } else {
169 self.margins.insert(
170 instrument_id,
171 MarginBalance::new(
172 Money::new(0.0, margin_maintenance.currency),
173 margin_maintenance,
174 instrument_id,
175 ),
176 );
177 }
178 self.recalculate_balance(margin_maintenance.currency);
179 }
180
181 #[must_use]
187 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
188 let margin_balance = self.margins.get(&instrument_id);
189 assert!(
190 margin_balance.is_some(),
191 "Cannot get maintenance_margin when no margin_balance"
192 );
193 margin_balance.unwrap().maintenance
194 }
195
196 pub fn calculate_initial_margin<T: Instrument>(
206 &mut self,
207 instrument: T,
208 quantity: Quantity,
209 price: Price,
210 use_quote_for_inverse: Option<bool>,
211 ) -> anyhow::Result<Money> {
212 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
213 let mut leverage = self.get_leverage(&instrument.id());
214 if leverage == Decimal::ZERO {
215 self.leverages
216 .insert(instrument.id(), self.default_leverage);
217 leverage = self.default_leverage;
218 }
219 let notional_decimal = notional.as_decimal();
220 let adjusted_notional = notional_decimal / leverage;
221 let margin_decimal = adjusted_notional * instrument.margin_init();
222
223 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
224 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
225 instrument.base_currency().unwrap()
226 } else {
227 instrument.quote_currency()
228 };
229
230 Money::from_decimal(margin_decimal, currency)
231 }
232
233 pub fn calculate_maintenance_margin<T: Instrument>(
243 &mut self,
244 instrument: T,
245 quantity: Quantity,
246 price: Price,
247 use_quote_for_inverse: Option<bool>,
248 ) -> anyhow::Result<Money> {
249 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
250 let mut leverage = self.get_leverage(&instrument.id());
251 if leverage == Decimal::ZERO {
252 self.leverages
253 .insert(instrument.id(), self.default_leverage);
254 leverage = self.default_leverage;
255 }
256 let notional_decimal = notional.as_decimal();
257 let adjusted_notional = notional_decimal / leverage;
258 let margin_decimal = adjusted_notional * instrument.margin_maint();
259
260 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
261 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
262 instrument.base_currency().unwrap()
263 } else {
264 instrument.quote_currency()
265 };
266
267 Money::from_decimal(margin_decimal, currency)
268 }
269
270 pub fn recalculate_balance(&mut self, currency: Currency) {
279 let current_balance = match self.balances.get(¤cy) {
280 Some(balance) => balance,
281 None => panic!("Cannot recalculate balance when no starting balance"),
282 };
283
284 let mut total_margin: MoneyRaw = 0;
285 for margin in self.margins.values() {
286 if margin.currency == currency {
287 total_margin = total_margin
288 .checked_add(margin.initial.raw)
289 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
290 .unwrap_or_else(|| {
291 panic!(
292 "Margin calculation overflow for currency {}: total would exceed maximum",
293 currency.code
294 )
295 });
296 }
297 }
298
299 let total_free = current_balance.total.raw - total_margin;
300 assert!(
302 total_free >= 0,
303 "Cannot recalculate balance when total_free is less than 0.0"
304 );
305 let new_balance = AccountBalance::new(
306 current_balance.total,
307 Money::from_raw(total_margin, currency),
308 Money::from_raw(total_free, currency),
309 );
310 self.balances.insert(currency, new_balance);
311 }
312}
313
314impl Deref for MarginAccount {
315 type Target = BaseAccount;
316
317 fn deref(&self) -> &Self::Target {
318 &self.base
319 }
320}
321
322impl DerefMut for MarginAccount {
323 fn deref_mut(&mut self) -> &mut Self::Target {
324 &mut self.base
325 }
326}
327
328impl Account for MarginAccount {
329 fn id(&self) -> AccountId {
330 self.id
331 }
332
333 fn account_type(&self) -> AccountType {
334 self.account_type
335 }
336
337 fn base_currency(&self) -> Option<Currency> {
338 self.base_currency
339 }
340
341 fn is_cash_account(&self) -> bool {
342 self.account_type == AccountType::Cash
343 }
344
345 fn is_margin_account(&self) -> bool {
346 self.account_type == AccountType::Margin
347 }
348
349 fn calculated_account_state(&self) -> bool {
350 false }
352
353 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
354 self.base_balance_total(currency)
355 }
356
357 fn balances_total(&self) -> HashMap<Currency, Money> {
358 self.base_balances_total()
359 }
360
361 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
362 self.base_balance_free(currency)
363 }
364
365 fn balances_free(&self) -> HashMap<Currency, Money> {
366 self.base_balances_free()
367 }
368
369 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
370 self.base_balance_locked(currency)
371 }
372
373 fn balances_locked(&self) -> HashMap<Currency, Money> {
374 self.base_balances_locked()
375 }
376
377 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
378 self.base_balance(currency)
379 }
380
381 fn last_event(&self) -> Option<AccountState> {
382 self.base_last_event()
383 }
384
385 fn events(&self) -> Vec<AccountState> {
386 self.events.clone()
387 }
388
389 fn event_count(&self) -> usize {
390 self.events.len()
391 }
392
393 fn currencies(&self) -> Vec<Currency> {
394 self.balances.keys().copied().collect()
395 }
396
397 fn starting_balances(&self) -> HashMap<Currency, Money> {
398 self.balances_starting.clone()
399 }
400
401 fn balances(&self) -> HashMap<Currency, AccountBalance> {
402 self.balances.clone()
403 }
404
405 fn apply(&mut self, event: AccountState) {
406 self.base_apply(event);
407 }
408
409 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
410 self.base.base_purge_account_events(ts_now, lookback_secs);
411 }
412
413 fn calculate_balance_locked(
414 &mut self,
415 instrument: InstrumentAny,
416 side: OrderSide,
417 quantity: Quantity,
418 price: Price,
419 use_quote_for_inverse: Option<bool>,
420 ) -> anyhow::Result<Money> {
421 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
422 }
423
424 fn calculate_pnls(
425 &self,
426 _instrument: InstrumentAny, fill: OrderFilled,
428 position: Option<Position>,
429 ) -> anyhow::Result<Vec<Money>> {
430 let mut pnls: Vec<Money> = Vec::new();
431
432 if let Some(ref pos) = position
433 && pos.quantity.is_positive()
434 && pos.entry != fill.order_side
435 {
436 let pnl_quantity = Quantity::from_raw(
439 fill.last_qty.raw.min(pos.quantity.raw),
440 fill.last_qty.precision,
441 );
442 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
443 pnls.push(pnl);
444 }
445
446 Ok(pnls)
447 }
448
449 fn calculate_commission(
450 &self,
451 instrument: InstrumentAny,
452 last_qty: Quantity,
453 last_px: Price,
454 liquidity_side: LiquiditySide,
455 use_quote_for_inverse: Option<bool>,
456 ) -> anyhow::Result<Money> {
457 self.base_calculate_commission(
458 instrument,
459 last_qty,
460 last_px,
461 liquidity_side,
462 use_quote_for_inverse,
463 )
464 }
465}
466
467impl PartialEq for MarginAccount {
468 fn eq(&self, other: &Self) -> bool {
469 self.id == other.id
470 }
471}
472
473impl Eq for MarginAccount {}
474
475impl Display for MarginAccount {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 write!(
478 f,
479 "MarginAccount(id={}, type={}, base={})",
480 self.id,
481 self.account_type,
482 self.base_currency.map_or_else(
483 || "None".to_string(),
484 |base_currency| format!("{}", base_currency.code)
485 ),
486 )
487 }
488}
489
490impl Hash for MarginAccount {
491 fn hash<H: Hasher>(&self, state: &mut H) {
492 self.id.hash(state);
493 }
494}
495
496#[cfg(test)]
500mod tests {
501 use std::collections::HashMap;
502
503 use nautilus_core::UnixNanos;
504 use rstest::rstest;
505 use rust_decimal::Decimal;
506
507 use crate::{
508 accounts::{Account, MarginAccount, stubs::*},
509 enums::{LiquiditySide, OrderSide, OrderType},
510 events::{AccountState, OrderFilled, account::stubs::*},
511 identifiers::{
512 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
513 VenueOrderId,
514 stubs::{uuid4, *},
515 },
516 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
517 position::Position,
518 types::{Currency, Money, Price, Quantity},
519 };
520
521 #[rstest]
522 fn test_display(margin_account: MarginAccount) {
523 assert_eq!(
524 margin_account.to_string(),
525 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
526 );
527 }
528
529 #[rstest]
530 fn test_base_account_properties(
531 margin_account: MarginAccount,
532 margin_account_state: AccountState,
533 ) {
534 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
535 assert_eq!(
536 margin_account.last_event(),
537 Some(margin_account_state.clone())
538 );
539 assert_eq!(margin_account.events(), vec![margin_account_state]);
540 assert_eq!(margin_account.event_count(), 1);
541 assert_eq!(
542 margin_account.balance_total(None),
543 Some(Money::from("1525000 USD"))
544 );
545 assert_eq!(
546 margin_account.balance_free(None),
547 Some(Money::from("1500000 USD"))
548 );
549 assert_eq!(
550 margin_account.balance_locked(None),
551 Some(Money::from("25000 USD"))
552 );
553 let mut balances_total_expected = HashMap::new();
554 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
555 assert_eq!(margin_account.balances_total(), balances_total_expected);
556 let mut balances_free_expected = HashMap::new();
557 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
558 assert_eq!(margin_account.balances_free(), balances_free_expected);
559 let mut balances_locked_expected = HashMap::new();
560 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
561 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
562 }
563
564 #[rstest]
565 fn test_set_default_leverage(mut margin_account: MarginAccount) {
566 assert_eq!(margin_account.default_leverage, Decimal::ONE);
567 margin_account.set_default_leverage(Decimal::from(10));
568 assert_eq!(margin_account.default_leverage, Decimal::from(10));
569 }
570
571 #[rstest]
572 fn test_get_leverage_default_leverage(
573 margin_account: MarginAccount,
574 instrument_id_aud_usd_sim: InstrumentId,
575 ) {
576 assert_eq!(
577 margin_account.get_leverage(&instrument_id_aud_usd_sim),
578 Decimal::ONE
579 );
580 }
581
582 #[rstest]
583 fn test_set_leverage(
584 mut margin_account: MarginAccount,
585 instrument_id_aud_usd_sim: InstrumentId,
586 ) {
587 assert_eq!(margin_account.leverages.len(), 0);
588 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
589 assert_eq!(margin_account.leverages.len(), 1);
590 assert_eq!(
591 margin_account.get_leverage(&instrument_id_aud_usd_sim),
592 Decimal::from(10)
593 );
594 }
595
596 #[rstest]
597 fn test_is_unleveraged_with_leverage_returns_false(
598 mut margin_account: MarginAccount,
599 instrument_id_aud_usd_sim: InstrumentId,
600 ) {
601 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
602 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
603 }
604
605 #[rstest]
606 fn test_is_unleveraged_with_no_leverage_returns_true(
607 mut margin_account: MarginAccount,
608 instrument_id_aud_usd_sim: InstrumentId,
609 ) {
610 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
611 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
612 }
613
614 #[rstest]
615 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
616 margin_account: MarginAccount,
617 instrument_id_aud_usd_sim: InstrumentId,
618 ) {
619 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
620 }
621
622 #[rstest]
623 fn test_update_margin_init(
624 mut margin_account: MarginAccount,
625 instrument_id_aud_usd_sim: InstrumentId,
626 ) {
627 assert_eq!(margin_account.margins.len(), 0);
628 let margin = Money::from("10000 USD");
629 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
630 assert_eq!(
631 margin_account.initial_margin(instrument_id_aud_usd_sim),
632 margin
633 );
634 let margins: Vec<Money> = margin_account
635 .margins
636 .values()
637 .map(|margin_balance| margin_balance.initial)
638 .collect();
639 assert_eq!(margins, vec![margin]);
640 }
641
642 #[rstest]
643 fn test_update_margin_maintenance(
644 mut margin_account: MarginAccount,
645 instrument_id_aud_usd_sim: InstrumentId,
646 ) {
647 let margin = Money::from("10000 USD");
648 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
649 assert_eq!(
650 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
651 margin
652 );
653 let margins: Vec<Money> = margin_account
654 .margins
655 .values()
656 .map(|margin_balance| margin_balance.maintenance)
657 .collect();
658 assert_eq!(margins, vec![margin]);
659 }
660
661 #[rstest]
662 fn test_calculate_margin_init_with_leverage(
663 mut margin_account: MarginAccount,
664 audusd_sim: CurrencyPair,
665 ) {
666 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
667 let result = margin_account
668 .calculate_initial_margin(
669 audusd_sim,
670 Quantity::from(100_000),
671 Price::from("0.8000"),
672 None,
673 )
674 .unwrap();
675 assert_eq!(result, Money::from("48.00 USD"));
676 }
677
678 #[rstest]
679 fn test_calculate_margin_init_with_default_leverage(
680 mut margin_account: MarginAccount,
681 audusd_sim: CurrencyPair,
682 ) {
683 margin_account.set_default_leverage(Decimal::from(10));
684 let result = margin_account
685 .calculate_initial_margin(
686 audusd_sim,
687 Quantity::from(100_000),
688 Price::from("0.8"),
689 None,
690 )
691 .unwrap();
692 assert_eq!(result, Money::from("240.00 USD"));
693 }
694
695 #[rstest]
696 fn test_calculate_margin_init_with_no_leverage_for_inverse(
697 mut margin_account: MarginAccount,
698 xbtusd_bitmex: CryptoPerpetual,
699 ) {
700 let result_use_quote_inverse_true = margin_account
701 .calculate_initial_margin(
702 xbtusd_bitmex,
703 Quantity::from(100_000),
704 Price::from("11493.60"),
705 Some(false),
706 )
707 .unwrap();
708 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
709 let result_use_quote_inverse_false = margin_account
710 .calculate_initial_margin(
711 xbtusd_bitmex,
712 Quantity::from(100_000),
713 Price::from("11493.60"),
714 Some(true),
715 )
716 .unwrap();
717 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
718 }
719
720 #[rstest]
721 fn test_calculate_margin_maintenance_with_no_leverage(
722 mut margin_account: MarginAccount,
723 xbtusd_bitmex: CryptoPerpetual,
724 ) {
725 let result = margin_account
726 .calculate_maintenance_margin(
727 xbtusd_bitmex,
728 Quantity::from(100_000),
729 Price::from("11493.60"),
730 None,
731 )
732 .unwrap();
733 assert_eq!(result, Money::from("0.03045173 BTC"));
734 }
735
736 #[rstest]
737 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
738 mut margin_account: MarginAccount,
739 audusd_sim: CurrencyPair,
740 ) {
741 margin_account.set_default_leverage(Decimal::from(50));
742 let result = margin_account
743 .calculate_maintenance_margin(
744 audusd_sim,
745 Quantity::from(1_000_000),
746 Price::from("1"),
747 None,
748 )
749 .unwrap();
750 assert_eq!(result, Money::from("600.00 USD"));
751 }
752
753 #[rstest]
754 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
755 mut margin_account: MarginAccount,
756 xbtusd_bitmex: CryptoPerpetual,
757 ) {
758 margin_account.set_default_leverage(Decimal::from(10));
759 let result = margin_account
760 .calculate_maintenance_margin(
761 xbtusd_bitmex,
762 Quantity::from(100_000),
763 Price::from("100000.00"),
764 None,
765 )
766 .unwrap();
767 assert_eq!(result, Money::from("0.00035000 BTC"));
768 }
769
770 #[rstest]
771 fn test_calculate_pnls_github_issue_2657() {
772 let account_state = margin_account_state();
774 let account = MarginAccount::new(account_state, false);
775
776 let btcusdt = currency_pair_btcusdt();
778 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
779
780 let fill1 = OrderFilled::new(
782 TraderId::from("TRADER-001"),
783 StrategyId::from("S-001"),
784 btcusdt.id,
785 ClientOrderId::from("O-1"),
786 VenueOrderId::from("V-1"),
787 AccountId::from("SIM-001"),
788 TradeId::from("T-1"),
789 OrderSide::Buy,
790 OrderType::Market,
791 Quantity::from("0.001"),
792 Price::from("50000.00"),
793 btcusdt.quote_currency,
794 LiquiditySide::Taker,
795 uuid4(),
796 UnixNanos::from(1_000_000_000),
797 UnixNanos::default(),
798 false,
799 Some(PositionId::from("P-GITHUB-2657")),
800 None,
801 );
802
803 let position = Position::new(&btcusdt_any, fill1);
804
805 let fill2 = OrderFilled::new(
807 TraderId::from("TRADER-001"),
808 StrategyId::from("S-001"),
809 btcusdt.id,
810 ClientOrderId::from("O-2"),
811 VenueOrderId::from("V-2"),
812 AccountId::from("SIM-001"),
813 TradeId::from("T-2"),
814 OrderSide::Sell,
815 OrderType::Market,
816 Quantity::from("0.002"), Price::from("50075.00"),
818 btcusdt.quote_currency,
819 LiquiditySide::Taker,
820 uuid4(),
821 UnixNanos::from(2_000_000_000),
822 UnixNanos::default(),
823 false,
824 Some(PositionId::from("P-GITHUB-2657")),
825 None,
826 );
827
828 let pnls = account
830 .calculate_pnls(btcusdt_any, fill2, Some(position))
831 .unwrap();
832
833 assert_eq!(pnls.len(), 1);
835
836 let expected_pnl = Money::from("0.075 USDT");
839 assert_eq!(pnls[0], expected_pnl);
840 }
841
842 #[rstest]
843 fn test_calculate_initial_margin_with_zero_leverage_falls_back_to_default(
844 mut margin_account: MarginAccount,
845 audusd_sim: CurrencyPair,
846 ) {
847 margin_account.set_default_leverage(Decimal::from(10));
849
850 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
852
853 let result = margin_account
855 .calculate_initial_margin(
856 audusd_sim,
857 Quantity::from(100_000),
858 Price::from("0.8"),
859 None,
860 )
861 .unwrap();
862
863 assert_eq!(result, Money::from("240.00 USD"));
866
867 assert_eq!(
869 margin_account.get_leverage(&audusd_sim.id),
870 Decimal::from(10)
871 );
872 }
873
874 #[rstest]
875 fn test_calculate_maintenance_margin_with_zero_leverage_falls_back_to_default(
876 mut margin_account: MarginAccount,
877 audusd_sim: CurrencyPair,
878 ) {
879 margin_account.set_default_leverage(Decimal::from(50));
881
882 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
884
885 let result = margin_account
887 .calculate_maintenance_margin(
888 audusd_sim,
889 Quantity::from(1_000_000),
890 Price::from("1"),
891 None,
892 )
893 .unwrap();
894
895 assert_eq!(result, Money::from("600.00 USD"));
898
899 assert_eq!(
901 margin_account.get_leverage(&audusd_sim.id),
902 Decimal::from(50)
903 );
904 }
905
906 #[rstest]
907 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
908 use nautilus_core::UnixNanos;
909
910 use crate::{
911 enums::{LiquiditySide, OrderSide, OrderType},
912 events::OrderFilled,
913 identifiers::{
914 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
915 stubs::uuid4,
916 },
917 instruments::InstrumentAny,
918 position::Position,
919 types::{Price, Quantity},
920 };
921
922 let account_state = margin_account_state();
924 let account = MarginAccount::new(account_state, false);
925
926 let btcusdt = currency_pair_btcusdt();
928 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
929
930 let fill1 = OrderFilled::new(
932 TraderId::from("TRADER-001"),
933 StrategyId::from("S-001"),
934 btcusdt.id,
935 ClientOrderId::from("O-1"),
936 VenueOrderId::from("V-1"),
937 AccountId::from("SIM-001"),
938 TradeId::from("T-1"),
939 OrderSide::Buy,
940 OrderType::Market,
941 Quantity::from("1.0"),
942 Price::from("50000.00"),
943 btcusdt.quote_currency,
944 LiquiditySide::Taker,
945 uuid4(),
946 UnixNanos::from(1_000_000_000),
947 UnixNanos::default(),
948 false,
949 Some(PositionId::from("P-123456")),
950 None,
951 );
952
953 let position = Position::new(&btcusdt_any, fill1);
954
955 let fill2 = OrderFilled::new(
957 TraderId::from("TRADER-001"),
958 StrategyId::from("S-001"),
959 btcusdt.id,
960 ClientOrderId::from("O-2"),
961 VenueOrderId::from("V-2"),
962 AccountId::from("SIM-001"),
963 TradeId::from("T-2"),
964 OrderSide::Buy, OrderType::Market,
966 Quantity::from("0.5"),
967 Price::from("51000.00"),
968 btcusdt.quote_currency,
969 LiquiditySide::Taker,
970 uuid4(),
971 UnixNanos::from(2_000_000_000),
972 UnixNanos::default(),
973 false,
974 Some(PositionId::from("P-123456")),
975 None,
976 );
977
978 let pnls = account
980 .calculate_pnls(btcusdt_any, fill2, Some(position))
981 .unwrap();
982
983 assert_eq!(pnls.len(), 0);
985 }
986}