1#![allow(dead_code)]
20
21use std::{
22 fmt::Display,
23 hash::{Hash, Hasher},
24 ops::{Deref, DerefMut},
25};
26
27use ahash::AHashMap;
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: AHashMap<InstrumentId, Decimal>,
49 pub margins: AHashMap<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: AHashMap::new(),
59 margins: AHashMap::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) -> AHashMap<InstrumentId, Money> {
96 let mut initial_margins: AHashMap<InstrumentId, Money> = AHashMap::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) -> AHashMap<InstrumentId, Money> {
105 let mut maintenance_margins: AHashMap<InstrumentId, Money> = AHashMap::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) -> AHashMap<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) -> AHashMap<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) -> AHashMap<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) -> AHashMap<Currency, Money> {
398 self.balances_starting.clone()
399 }
400
401 fn balances(&self) -> AHashMap<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)]
497mod tests {
498 use ahash::AHashMap;
499 use nautilus_core::UnixNanos;
500 use rstest::rstest;
501 use rust_decimal::Decimal;
502
503 use crate::{
504 accounts::{Account, MarginAccount, stubs::*},
505 enums::{LiquiditySide, OrderSide, OrderType},
506 events::{AccountState, OrderFilled, account::stubs::*},
507 identifiers::{
508 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
509 VenueOrderId,
510 stubs::{uuid4, *},
511 },
512 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
513 position::Position,
514 types::{Currency, Money, Price, Quantity},
515 };
516
517 #[rstest]
518 fn test_display(margin_account: MarginAccount) {
519 assert_eq!(
520 margin_account.to_string(),
521 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
522 );
523 }
524
525 #[rstest]
526 fn test_base_account_properties(
527 margin_account: MarginAccount,
528 margin_account_state: AccountState,
529 ) {
530 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
531 assert_eq!(
532 margin_account.last_event(),
533 Some(margin_account_state.clone())
534 );
535 assert_eq!(margin_account.events(), vec![margin_account_state]);
536 assert_eq!(margin_account.event_count(), 1);
537 assert_eq!(
538 margin_account.balance_total(None),
539 Some(Money::from("1525000 USD"))
540 );
541 assert_eq!(
542 margin_account.balance_free(None),
543 Some(Money::from("1500000 USD"))
544 );
545 assert_eq!(
546 margin_account.balance_locked(None),
547 Some(Money::from("25000 USD"))
548 );
549 let mut balances_total_expected = AHashMap::new();
550 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
551 assert_eq!(margin_account.balances_total(), balances_total_expected);
552 let mut balances_free_expected = AHashMap::new();
553 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
554 assert_eq!(margin_account.balances_free(), balances_free_expected);
555 let mut balances_locked_expected = AHashMap::new();
556 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
557 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
558 }
559
560 #[rstest]
561 fn test_set_default_leverage(mut margin_account: MarginAccount) {
562 assert_eq!(margin_account.default_leverage, Decimal::ONE);
563 margin_account.set_default_leverage(Decimal::from(10));
564 assert_eq!(margin_account.default_leverage, Decimal::from(10));
565 }
566
567 #[rstest]
568 fn test_get_leverage_default_leverage(
569 margin_account: MarginAccount,
570 instrument_id_aud_usd_sim: InstrumentId,
571 ) {
572 assert_eq!(
573 margin_account.get_leverage(&instrument_id_aud_usd_sim),
574 Decimal::ONE
575 );
576 }
577
578 #[rstest]
579 fn test_set_leverage(
580 mut margin_account: MarginAccount,
581 instrument_id_aud_usd_sim: InstrumentId,
582 ) {
583 assert_eq!(margin_account.leverages.len(), 0);
584 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
585 assert_eq!(margin_account.leverages.len(), 1);
586 assert_eq!(
587 margin_account.get_leverage(&instrument_id_aud_usd_sim),
588 Decimal::from(10)
589 );
590 }
591
592 #[rstest]
593 fn test_is_unleveraged_with_leverage_returns_false(
594 mut margin_account: MarginAccount,
595 instrument_id_aud_usd_sim: InstrumentId,
596 ) {
597 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
598 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
599 }
600
601 #[rstest]
602 fn test_is_unleveraged_with_no_leverage_returns_true(
603 mut margin_account: MarginAccount,
604 instrument_id_aud_usd_sim: InstrumentId,
605 ) {
606 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
607 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
608 }
609
610 #[rstest]
611 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
612 margin_account: MarginAccount,
613 instrument_id_aud_usd_sim: InstrumentId,
614 ) {
615 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
616 }
617
618 #[rstest]
619 fn test_update_margin_init(
620 mut margin_account: MarginAccount,
621 instrument_id_aud_usd_sim: InstrumentId,
622 ) {
623 assert_eq!(margin_account.margins.len(), 0);
624 let margin = Money::from("10000 USD");
625 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
626 assert_eq!(
627 margin_account.initial_margin(instrument_id_aud_usd_sim),
628 margin
629 );
630 let margins: Vec<Money> = margin_account
631 .margins
632 .values()
633 .map(|margin_balance| margin_balance.initial)
634 .collect();
635 assert_eq!(margins, vec![margin]);
636 }
637
638 #[rstest]
639 fn test_update_margin_maintenance(
640 mut margin_account: MarginAccount,
641 instrument_id_aud_usd_sim: InstrumentId,
642 ) {
643 let margin = Money::from("10000 USD");
644 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
645 assert_eq!(
646 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
647 margin
648 );
649 let margins: Vec<Money> = margin_account
650 .margins
651 .values()
652 .map(|margin_balance| margin_balance.maintenance)
653 .collect();
654 assert_eq!(margins, vec![margin]);
655 }
656
657 #[rstest]
658 fn test_calculate_margin_init_with_leverage(
659 mut margin_account: MarginAccount,
660 audusd_sim: CurrencyPair,
661 ) {
662 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
663 let result = margin_account
664 .calculate_initial_margin(
665 audusd_sim,
666 Quantity::from(100_000),
667 Price::from("0.8000"),
668 None,
669 )
670 .unwrap();
671 assert_eq!(result, Money::from("48.00 USD"));
672 }
673
674 #[rstest]
675 fn test_calculate_margin_init_with_default_leverage(
676 mut margin_account: MarginAccount,
677 audusd_sim: CurrencyPair,
678 ) {
679 margin_account.set_default_leverage(Decimal::from(10));
680 let result = margin_account
681 .calculate_initial_margin(
682 audusd_sim,
683 Quantity::from(100_000),
684 Price::from("0.8"),
685 None,
686 )
687 .unwrap();
688 assert_eq!(result, Money::from("240.00 USD"));
689 }
690
691 #[rstest]
692 fn test_calculate_margin_init_with_no_leverage_for_inverse(
693 mut margin_account: MarginAccount,
694 xbtusd_bitmex: CryptoPerpetual,
695 ) {
696 let result_use_quote_inverse_true = margin_account
697 .calculate_initial_margin(
698 xbtusd_bitmex,
699 Quantity::from(100_000),
700 Price::from("11493.60"),
701 Some(false),
702 )
703 .unwrap();
704 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
705 let result_use_quote_inverse_false = margin_account
706 .calculate_initial_margin(
707 xbtusd_bitmex,
708 Quantity::from(100_000),
709 Price::from("11493.60"),
710 Some(true),
711 )
712 .unwrap();
713 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
714 }
715
716 #[rstest]
717 fn test_calculate_margin_maintenance_with_no_leverage(
718 mut margin_account: MarginAccount,
719 xbtusd_bitmex: CryptoPerpetual,
720 ) {
721 let result = margin_account
722 .calculate_maintenance_margin(
723 xbtusd_bitmex,
724 Quantity::from(100_000),
725 Price::from("11493.60"),
726 None,
727 )
728 .unwrap();
729 assert_eq!(result, Money::from("0.03045173 BTC"));
730 }
731
732 #[rstest]
733 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
734 mut margin_account: MarginAccount,
735 audusd_sim: CurrencyPair,
736 ) {
737 margin_account.set_default_leverage(Decimal::from(50));
738 let result = margin_account
739 .calculate_maintenance_margin(
740 audusd_sim,
741 Quantity::from(1_000_000),
742 Price::from("1"),
743 None,
744 )
745 .unwrap();
746 assert_eq!(result, Money::from("600.00 USD"));
747 }
748
749 #[rstest]
750 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
751 mut margin_account: MarginAccount,
752 xbtusd_bitmex: CryptoPerpetual,
753 ) {
754 margin_account.set_default_leverage(Decimal::from(10));
755 let result = margin_account
756 .calculate_maintenance_margin(
757 xbtusd_bitmex,
758 Quantity::from(100_000),
759 Price::from("100000.00"),
760 None,
761 )
762 .unwrap();
763 assert_eq!(result, Money::from("0.00035000 BTC"));
764 }
765
766 #[rstest]
767 fn test_calculate_pnls_github_issue_2657() {
768 let account_state = margin_account_state();
770 let account = MarginAccount::new(account_state, false);
771
772 let btcusdt = currency_pair_btcusdt();
774 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
775
776 let fill1 = OrderFilled::new(
778 TraderId::from("TRADER-001"),
779 StrategyId::from("S-001"),
780 btcusdt.id,
781 ClientOrderId::from("O-1"),
782 VenueOrderId::from("V-1"),
783 AccountId::from("SIM-001"),
784 TradeId::from("T-1"),
785 OrderSide::Buy,
786 OrderType::Market,
787 Quantity::from("0.001"),
788 Price::from("50000.00"),
789 btcusdt.quote_currency,
790 LiquiditySide::Taker,
791 uuid4(),
792 UnixNanos::from(1_000_000_000),
793 UnixNanos::default(),
794 false,
795 Some(PositionId::from("P-GITHUB-2657")),
796 None,
797 );
798
799 let position = Position::new(&btcusdt_any, fill1);
800
801 let fill2 = OrderFilled::new(
803 TraderId::from("TRADER-001"),
804 StrategyId::from("S-001"),
805 btcusdt.id,
806 ClientOrderId::from("O-2"),
807 VenueOrderId::from("V-2"),
808 AccountId::from("SIM-001"),
809 TradeId::from("T-2"),
810 OrderSide::Sell,
811 OrderType::Market,
812 Quantity::from("0.002"), Price::from("50075.00"),
814 btcusdt.quote_currency,
815 LiquiditySide::Taker,
816 uuid4(),
817 UnixNanos::from(2_000_000_000),
818 UnixNanos::default(),
819 false,
820 Some(PositionId::from("P-GITHUB-2657")),
821 None,
822 );
823
824 let pnls = account
826 .calculate_pnls(btcusdt_any, fill2, Some(position))
827 .unwrap();
828
829 assert_eq!(pnls.len(), 1);
831
832 let expected_pnl = Money::from("0.075 USDT");
835 assert_eq!(pnls[0], expected_pnl);
836 }
837
838 #[rstest]
839 fn test_calculate_initial_margin_with_zero_leverage_falls_back_to_default(
840 mut margin_account: MarginAccount,
841 audusd_sim: CurrencyPair,
842 ) {
843 margin_account.set_default_leverage(Decimal::from(10));
845
846 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
848
849 let result = margin_account
851 .calculate_initial_margin(
852 audusd_sim,
853 Quantity::from(100_000),
854 Price::from("0.8"),
855 None,
856 )
857 .unwrap();
858
859 assert_eq!(result, Money::from("240.00 USD"));
862
863 assert_eq!(
865 margin_account.get_leverage(&audusd_sim.id),
866 Decimal::from(10)
867 );
868 }
869
870 #[rstest]
871 fn test_calculate_maintenance_margin_with_zero_leverage_falls_back_to_default(
872 mut margin_account: MarginAccount,
873 audusd_sim: CurrencyPair,
874 ) {
875 margin_account.set_default_leverage(Decimal::from(50));
877
878 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
880
881 let result = margin_account
883 .calculate_maintenance_margin(
884 audusd_sim,
885 Quantity::from(1_000_000),
886 Price::from("1"),
887 None,
888 )
889 .unwrap();
890
891 assert_eq!(result, Money::from("600.00 USD"));
894
895 assert_eq!(
897 margin_account.get_leverage(&audusd_sim.id),
898 Decimal::from(50)
899 );
900 }
901
902 #[rstest]
903 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
904 use nautilus_core::UnixNanos;
905
906 use crate::{
907 enums::{LiquiditySide, OrderSide, OrderType},
908 events::OrderFilled,
909 identifiers::{
910 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
911 stubs::uuid4,
912 },
913 instruments::InstrumentAny,
914 position::Position,
915 types::{Price, Quantity},
916 };
917
918 let account_state = margin_account_state();
920 let account = MarginAccount::new(account_state, false);
921
922 let btcusdt = currency_pair_btcusdt();
924 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
925
926 let fill1 = OrderFilled::new(
928 TraderId::from("TRADER-001"),
929 StrategyId::from("S-001"),
930 btcusdt.id,
931 ClientOrderId::from("O-1"),
932 VenueOrderId::from("V-1"),
933 AccountId::from("SIM-001"),
934 TradeId::from("T-1"),
935 OrderSide::Buy,
936 OrderType::Market,
937 Quantity::from("1.0"),
938 Price::from("50000.00"),
939 btcusdt.quote_currency,
940 LiquiditySide::Taker,
941 uuid4(),
942 UnixNanos::from(1_000_000_000),
943 UnixNanos::default(),
944 false,
945 Some(PositionId::from("P-123456")),
946 None,
947 );
948
949 let position = Position::new(&btcusdt_any, fill1);
950
951 let fill2 = OrderFilled::new(
953 TraderId::from("TRADER-001"),
954 StrategyId::from("S-001"),
955 btcusdt.id,
956 ClientOrderId::from("O-2"),
957 VenueOrderId::from("V-2"),
958 AccountId::from("SIM-001"),
959 TradeId::from("T-2"),
960 OrderSide::Buy, OrderType::Market,
962 Quantity::from("0.5"),
963 Price::from("51000.00"),
964 btcusdt.quote_currency,
965 LiquiditySide::Taker,
966 uuid4(),
967 UnixNanos::from(2_000_000_000),
968 UnixNanos::default(),
969 false,
970 Some(PositionId::from("P-123456")),
971 None,
972 );
973
974 let pnls = account
976 .calculate_pnls(btcusdt_any, fill2, Some(position))
977 .unwrap();
978
979 assert_eq!(pnls.len(), 0);
981 }
982}