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 #[must_use]
198 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
199 self.margins.get(instrument_id).copied()
200 }
201
202 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
204 self.margins
205 .insert(margin_balance.instrument_id, margin_balance);
206 self.recalculate_balance(margin_balance.currency);
207 }
208
209 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
211 if let Some(margin_balance) = self.margins.remove(&instrument_id) {
212 self.recalculate_balance(margin_balance.currency);
213 }
214 }
215
216 pub fn calculate_initial_margin<T: Instrument>(
226 &mut self,
227 instrument: T,
228 quantity: Quantity,
229 price: Price,
230 use_quote_for_inverse: Option<bool>,
231 ) -> anyhow::Result<Money> {
232 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
233 let mut leverage = self.get_leverage(&instrument.id());
234 if leverage == Decimal::ZERO {
235 self.leverages
236 .insert(instrument.id(), self.default_leverage);
237 leverage = self.default_leverage;
238 }
239 let notional_decimal = notional.as_decimal();
240 let adjusted_notional = notional_decimal / leverage;
241 let margin_decimal = adjusted_notional * instrument.margin_init();
242
243 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
244 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
245 instrument.base_currency().unwrap()
246 } else {
247 instrument.quote_currency()
248 };
249
250 Money::from_decimal(margin_decimal, currency)
251 }
252
253 pub fn calculate_maintenance_margin<T: Instrument>(
263 &mut self,
264 instrument: T,
265 quantity: Quantity,
266 price: Price,
267 use_quote_for_inverse: Option<bool>,
268 ) -> anyhow::Result<Money> {
269 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
270 let mut leverage = self.get_leverage(&instrument.id());
271 if leverage == Decimal::ZERO {
272 self.leverages
273 .insert(instrument.id(), self.default_leverage);
274 leverage = self.default_leverage;
275 }
276 let notional_decimal = notional.as_decimal();
277 let adjusted_notional = notional_decimal / leverage;
278 let margin_decimal = adjusted_notional * instrument.margin_maint();
279
280 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
281 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
282 instrument.base_currency().unwrap()
283 } else {
284 instrument.quote_currency()
285 };
286
287 Money::from_decimal(margin_decimal, currency)
288 }
289
290 pub fn recalculate_balance(&mut self, currency: Currency) {
297 let current_balance = match self.balances.get(¤cy) {
298 Some(balance) => *balance,
299 None => {
300 let zero = Money::from_raw(0, currency);
303 AccountBalance::new(zero, zero, zero)
304 }
305 };
306
307 let mut total_margin: MoneyRaw = 0;
308 for margin in self.margins.values() {
309 if margin.currency == currency {
310 total_margin = total_margin
311 .checked_add(margin.initial.raw)
312 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
313 .unwrap_or_else(|| {
314 panic!(
315 "Margin calculation overflow for currency {}: total would exceed maximum",
316 currency.code
317 )
318 });
319 }
320 }
321
322 let total_free = if total_margin > current_balance.total.raw {
325 total_margin = current_balance.total.raw;
326 0
327 } else {
328 current_balance.total.raw - total_margin
329 };
330
331 let new_balance = AccountBalance::new(
332 current_balance.total,
333 Money::from_raw(total_margin, currency),
334 Money::from_raw(total_free, currency),
335 );
336 self.balances.insert(currency, new_balance);
337 }
338}
339
340impl Deref for MarginAccount {
341 type Target = BaseAccount;
342
343 fn deref(&self) -> &Self::Target {
344 &self.base
345 }
346}
347
348impl DerefMut for MarginAccount {
349 fn deref_mut(&mut self) -> &mut Self::Target {
350 &mut self.base
351 }
352}
353
354impl Account for MarginAccount {
355 fn id(&self) -> AccountId {
356 self.id
357 }
358
359 fn account_type(&self) -> AccountType {
360 self.account_type
361 }
362
363 fn base_currency(&self) -> Option<Currency> {
364 self.base_currency
365 }
366
367 fn is_cash_account(&self) -> bool {
368 self.account_type == AccountType::Cash
369 }
370
371 fn is_margin_account(&self) -> bool {
372 self.account_type == AccountType::Margin
373 }
374
375 fn calculated_account_state(&self) -> bool {
376 false }
378
379 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
380 self.base_balance_total(currency)
381 }
382
383 fn balances_total(&self) -> AHashMap<Currency, Money> {
384 self.base_balances_total()
385 }
386
387 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
388 self.base_balance_free(currency)
389 }
390
391 fn balances_free(&self) -> AHashMap<Currency, Money> {
392 self.base_balances_free()
393 }
394
395 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
396 self.base_balance_locked(currency)
397 }
398
399 fn balances_locked(&self) -> AHashMap<Currency, Money> {
400 self.base_balances_locked()
401 }
402
403 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
404 self.base_balance(currency)
405 }
406
407 fn last_event(&self) -> Option<AccountState> {
408 self.base_last_event()
409 }
410
411 fn events(&self) -> Vec<AccountState> {
412 self.events.clone()
413 }
414
415 fn event_count(&self) -> usize {
416 self.events.len()
417 }
418
419 fn currencies(&self) -> Vec<Currency> {
420 self.balances.keys().copied().collect()
421 }
422
423 fn starting_balances(&self) -> AHashMap<Currency, Money> {
424 self.balances_starting.clone()
425 }
426
427 fn balances(&self) -> AHashMap<Currency, AccountBalance> {
428 self.balances.clone()
429 }
430
431 fn apply(&mut self, event: AccountState) {
432 self.base_apply(event);
433 }
434
435 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
436 self.base.base_purge_account_events(ts_now, lookback_secs);
437 }
438
439 fn calculate_balance_locked(
440 &mut self,
441 instrument: InstrumentAny,
442 side: OrderSide,
443 quantity: Quantity,
444 price: Price,
445 use_quote_for_inverse: Option<bool>,
446 ) -> anyhow::Result<Money> {
447 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
448 }
449
450 fn calculate_pnls(
451 &self,
452 _instrument: InstrumentAny, fill: OrderFilled,
454 position: Option<Position>,
455 ) -> anyhow::Result<Vec<Money>> {
456 let mut pnls: Vec<Money> = Vec::new();
457
458 if let Some(ref pos) = position
459 && pos.quantity.is_positive()
460 && pos.entry != fill.order_side
461 {
462 let pnl_quantity = Quantity::from_raw(
465 fill.last_qty.raw.min(pos.quantity.raw),
466 fill.last_qty.precision,
467 );
468 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
469 pnls.push(pnl);
470 }
471
472 Ok(pnls)
473 }
474
475 fn calculate_commission(
476 &self,
477 instrument: InstrumentAny,
478 last_qty: Quantity,
479 last_px: Price,
480 liquidity_side: LiquiditySide,
481 use_quote_for_inverse: Option<bool>,
482 ) -> anyhow::Result<Money> {
483 self.base_calculate_commission(
484 instrument,
485 last_qty,
486 last_px,
487 liquidity_side,
488 use_quote_for_inverse,
489 )
490 }
491}
492
493impl PartialEq for MarginAccount {
494 fn eq(&self, other: &Self) -> bool {
495 self.id == other.id
496 }
497}
498
499impl Eq for MarginAccount {}
500
501impl Display for MarginAccount {
502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503 write!(
504 f,
505 "MarginAccount(id={}, type={}, base={})",
506 self.id,
507 self.account_type,
508 self.base_currency.map_or_else(
509 || "None".to_string(),
510 |base_currency| format!("{}", base_currency.code)
511 ),
512 )
513 }
514}
515
516impl Hash for MarginAccount {
517 fn hash<H: Hasher>(&self, state: &mut H) {
518 self.id.hash(state);
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use ahash::AHashMap;
525 use nautilus_core::UnixNanos;
526 use rstest::rstest;
527 use rust_decimal::Decimal;
528
529 use crate::{
530 accounts::{Account, MarginAccount, stubs::*},
531 enums::{LiquiditySide, OrderSide, OrderType},
532 events::{AccountState, OrderFilled, account::stubs::*},
533 identifiers::{
534 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
535 VenueOrderId,
536 stubs::{uuid4, *},
537 },
538 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
539 position::Position,
540 types::{Currency, MarginBalance, Money, Price, Quantity},
541 };
542
543 #[rstest]
544 fn test_display(margin_account: MarginAccount) {
545 assert_eq!(
546 margin_account.to_string(),
547 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
548 );
549 }
550
551 #[rstest]
552 fn test_base_account_properties(
553 margin_account: MarginAccount,
554 margin_account_state: AccountState,
555 ) {
556 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
557 assert_eq!(
558 margin_account.last_event(),
559 Some(margin_account_state.clone())
560 );
561 assert_eq!(margin_account.events(), vec![margin_account_state]);
562 assert_eq!(margin_account.event_count(), 1);
563 assert_eq!(
564 margin_account.balance_total(None),
565 Some(Money::from("1525000 USD"))
566 );
567 assert_eq!(
568 margin_account.balance_free(None),
569 Some(Money::from("1500000 USD"))
570 );
571 assert_eq!(
572 margin_account.balance_locked(None),
573 Some(Money::from("25000 USD"))
574 );
575 let mut balances_total_expected = AHashMap::new();
576 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
577 assert_eq!(margin_account.balances_total(), balances_total_expected);
578 let mut balances_free_expected = AHashMap::new();
579 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
580 assert_eq!(margin_account.balances_free(), balances_free_expected);
581 let mut balances_locked_expected = AHashMap::new();
582 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
583 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
584 }
585
586 #[rstest]
587 fn test_set_default_leverage(mut margin_account: MarginAccount) {
588 assert_eq!(margin_account.default_leverage, Decimal::ONE);
589 margin_account.set_default_leverage(Decimal::from(10));
590 assert_eq!(margin_account.default_leverage, Decimal::from(10));
591 }
592
593 #[rstest]
594 fn test_get_leverage_default_leverage(
595 margin_account: MarginAccount,
596 instrument_id_aud_usd_sim: InstrumentId,
597 ) {
598 assert_eq!(
599 margin_account.get_leverage(&instrument_id_aud_usd_sim),
600 Decimal::ONE
601 );
602 }
603
604 #[rstest]
605 fn test_set_leverage(
606 mut margin_account: MarginAccount,
607 instrument_id_aud_usd_sim: InstrumentId,
608 ) {
609 assert_eq!(margin_account.leverages.len(), 0);
610 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
611 assert_eq!(margin_account.leverages.len(), 1);
612 assert_eq!(
613 margin_account.get_leverage(&instrument_id_aud_usd_sim),
614 Decimal::from(10)
615 );
616 }
617
618 #[rstest]
619 fn test_is_unleveraged_with_leverage_returns_false(
620 mut margin_account: MarginAccount,
621 instrument_id_aud_usd_sim: InstrumentId,
622 ) {
623 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
624 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
625 }
626
627 #[rstest]
628 fn test_is_unleveraged_with_no_leverage_returns_true(
629 mut margin_account: MarginAccount,
630 instrument_id_aud_usd_sim: InstrumentId,
631 ) {
632 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
633 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
634 }
635
636 #[rstest]
637 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
638 margin_account: MarginAccount,
639 instrument_id_aud_usd_sim: InstrumentId,
640 ) {
641 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
642 }
643
644 #[rstest]
645 fn test_update_margin_init(
646 mut margin_account: MarginAccount,
647 instrument_id_aud_usd_sim: InstrumentId,
648 ) {
649 assert_eq!(margin_account.margins.len(), 0);
650 let margin = Money::from("10000 USD");
651 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
652 assert_eq!(
653 margin_account.initial_margin(instrument_id_aud_usd_sim),
654 margin
655 );
656 let margins: Vec<Money> = margin_account
657 .margins
658 .values()
659 .map(|margin_balance| margin_balance.initial)
660 .collect();
661 assert_eq!(margins, vec![margin]);
662 }
663
664 #[rstest]
665 fn test_update_margin_maintenance(
666 mut margin_account: MarginAccount,
667 instrument_id_aud_usd_sim: InstrumentId,
668 ) {
669 let margin = Money::from("10000 USD");
670 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
671 assert_eq!(
672 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
673 margin
674 );
675 let margins: Vec<Money> = margin_account
676 .margins
677 .values()
678 .map(|margin_balance| margin_balance.maintenance)
679 .collect();
680 assert_eq!(margins, vec![margin]);
681 }
682
683 #[rstest]
684 fn test_calculate_margin_init_with_leverage(
685 mut margin_account: MarginAccount,
686 audusd_sim: CurrencyPair,
687 ) {
688 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
689 let result = margin_account
690 .calculate_initial_margin(
691 audusd_sim,
692 Quantity::from(100_000),
693 Price::from("0.8000"),
694 None,
695 )
696 .unwrap();
697 assert_eq!(result, Money::from("48.00 USD"));
698 }
699
700 #[rstest]
701 fn test_calculate_margin_init_with_default_leverage(
702 mut margin_account: MarginAccount,
703 audusd_sim: CurrencyPair,
704 ) {
705 margin_account.set_default_leverage(Decimal::from(10));
706 let result = margin_account
707 .calculate_initial_margin(
708 audusd_sim,
709 Quantity::from(100_000),
710 Price::from("0.8"),
711 None,
712 )
713 .unwrap();
714 assert_eq!(result, Money::from("240.00 USD"));
715 }
716
717 #[rstest]
718 fn test_calculate_margin_init_with_no_leverage_for_inverse(
719 mut margin_account: MarginAccount,
720 xbtusd_bitmex: CryptoPerpetual,
721 ) {
722 let result_use_quote_inverse_true = margin_account
723 .calculate_initial_margin(
724 xbtusd_bitmex,
725 Quantity::from(100_000),
726 Price::from("11493.60"),
727 Some(false),
728 )
729 .unwrap();
730 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
731 let result_use_quote_inverse_false = margin_account
732 .calculate_initial_margin(
733 xbtusd_bitmex,
734 Quantity::from(100_000),
735 Price::from("11493.60"),
736 Some(true),
737 )
738 .unwrap();
739 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
740 }
741
742 #[rstest]
743 fn test_calculate_margin_maintenance_with_no_leverage(
744 mut margin_account: MarginAccount,
745 xbtusd_bitmex: CryptoPerpetual,
746 ) {
747 let result = margin_account
748 .calculate_maintenance_margin(
749 xbtusd_bitmex,
750 Quantity::from(100_000),
751 Price::from("11493.60"),
752 None,
753 )
754 .unwrap();
755 assert_eq!(result, Money::from("0.03045173 BTC"));
756 }
757
758 #[rstest]
759 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
760 mut margin_account: MarginAccount,
761 audusd_sim: CurrencyPair,
762 ) {
763 margin_account.set_default_leverage(Decimal::from(50));
764 let result = margin_account
765 .calculate_maintenance_margin(
766 audusd_sim,
767 Quantity::from(1_000_000),
768 Price::from("1"),
769 None,
770 )
771 .unwrap();
772 assert_eq!(result, Money::from("600.00 USD"));
773 }
774
775 #[rstest]
776 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
777 mut margin_account: MarginAccount,
778 xbtusd_bitmex: CryptoPerpetual,
779 ) {
780 margin_account.set_default_leverage(Decimal::from(10));
781 let result = margin_account
782 .calculate_maintenance_margin(
783 xbtusd_bitmex,
784 Quantity::from(100_000),
785 Price::from("100000.00"),
786 None,
787 )
788 .unwrap();
789 assert_eq!(result, Money::from("0.00035000 BTC"));
790 }
791
792 #[rstest]
793 fn test_calculate_pnls_github_issue_2657() {
794 let account_state = margin_account_state();
796 let account = MarginAccount::new(account_state, false);
797
798 let btcusdt = currency_pair_btcusdt();
800 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
801
802 let fill1 = OrderFilled::new(
804 TraderId::from("TRADER-001"),
805 StrategyId::from("S-001"),
806 btcusdt.id,
807 ClientOrderId::from("O-1"),
808 VenueOrderId::from("V-1"),
809 AccountId::from("SIM-001"),
810 TradeId::from("T-1"),
811 OrderSide::Buy,
812 OrderType::Market,
813 Quantity::from("0.001"),
814 Price::from("50000.00"),
815 btcusdt.quote_currency,
816 LiquiditySide::Taker,
817 uuid4(),
818 UnixNanos::from(1_000_000_000),
819 UnixNanos::default(),
820 false,
821 Some(PositionId::from("P-GITHUB-2657")),
822 None,
823 );
824
825 let position = Position::new(&btcusdt_any, fill1);
826
827 let fill2 = OrderFilled::new(
829 TraderId::from("TRADER-001"),
830 StrategyId::from("S-001"),
831 btcusdt.id,
832 ClientOrderId::from("O-2"),
833 VenueOrderId::from("V-2"),
834 AccountId::from("SIM-001"),
835 TradeId::from("T-2"),
836 OrderSide::Sell,
837 OrderType::Market,
838 Quantity::from("0.002"), Price::from("50075.00"),
840 btcusdt.quote_currency,
841 LiquiditySide::Taker,
842 uuid4(),
843 UnixNanos::from(2_000_000_000),
844 UnixNanos::default(),
845 false,
846 Some(PositionId::from("P-GITHUB-2657")),
847 None,
848 );
849
850 let pnls = account
852 .calculate_pnls(btcusdt_any, fill2, Some(position))
853 .unwrap();
854
855 assert_eq!(pnls.len(), 1);
857
858 let expected_pnl = Money::from("0.075 USDT");
861 assert_eq!(pnls[0], expected_pnl);
862 }
863
864 #[rstest]
865 fn test_calculate_initial_margin_with_zero_leverage_falls_back_to_default(
866 mut margin_account: MarginAccount,
867 audusd_sim: CurrencyPair,
868 ) {
869 margin_account.set_default_leverage(Decimal::from(10));
871
872 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
874
875 let result = margin_account
877 .calculate_initial_margin(
878 audusd_sim,
879 Quantity::from(100_000),
880 Price::from("0.8"),
881 None,
882 )
883 .unwrap();
884
885 assert_eq!(result, Money::from("240.00 USD"));
888
889 assert_eq!(
891 margin_account.get_leverage(&audusd_sim.id),
892 Decimal::from(10)
893 );
894 }
895
896 #[rstest]
897 fn test_calculate_maintenance_margin_with_zero_leverage_falls_back_to_default(
898 mut margin_account: MarginAccount,
899 audusd_sim: CurrencyPair,
900 ) {
901 margin_account.set_default_leverage(Decimal::from(50));
903
904 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
906
907 let result = margin_account
909 .calculate_maintenance_margin(
910 audusd_sim,
911 Quantity::from(1_000_000),
912 Price::from("1"),
913 None,
914 )
915 .unwrap();
916
917 assert_eq!(result, Money::from("600.00 USD"));
920
921 assert_eq!(
923 margin_account.get_leverage(&audusd_sim.id),
924 Decimal::from(50)
925 );
926 }
927
928 #[rstest]
929 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
930 use nautilus_core::UnixNanos;
931
932 use crate::{
933 enums::{LiquiditySide, OrderSide, OrderType},
934 events::OrderFilled,
935 identifiers::{
936 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
937 stubs::uuid4,
938 },
939 instruments::InstrumentAny,
940 position::Position,
941 types::{Price, Quantity},
942 };
943
944 let account_state = margin_account_state();
946 let account = MarginAccount::new(account_state, false);
947
948 let btcusdt = currency_pair_btcusdt();
950 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
951
952 let fill1 = OrderFilled::new(
954 TraderId::from("TRADER-001"),
955 StrategyId::from("S-001"),
956 btcusdt.id,
957 ClientOrderId::from("O-1"),
958 VenueOrderId::from("V-1"),
959 AccountId::from("SIM-001"),
960 TradeId::from("T-1"),
961 OrderSide::Buy,
962 OrderType::Market,
963 Quantity::from("1.0"),
964 Price::from("50000.00"),
965 btcusdt.quote_currency,
966 LiquiditySide::Taker,
967 uuid4(),
968 UnixNanos::from(1_000_000_000),
969 UnixNanos::default(),
970 false,
971 Some(PositionId::from("P-123456")),
972 None,
973 );
974
975 let position = Position::new(&btcusdt_any, fill1);
976
977 let fill2 = OrderFilled::new(
979 TraderId::from("TRADER-001"),
980 StrategyId::from("S-001"),
981 btcusdt.id,
982 ClientOrderId::from("O-2"),
983 VenueOrderId::from("V-2"),
984 AccountId::from("SIM-001"),
985 TradeId::from("T-2"),
986 OrderSide::Buy, OrderType::Market,
988 Quantity::from("0.5"),
989 Price::from("51000.00"),
990 btcusdt.quote_currency,
991 LiquiditySide::Taker,
992 uuid4(),
993 UnixNanos::from(2_000_000_000),
994 UnixNanos::default(),
995 false,
996 Some(PositionId::from("P-123456")),
997 None,
998 );
999
1000 let pnls = account
1002 .calculate_pnls(btcusdt_any, fill2, Some(position))
1003 .unwrap();
1004
1005 assert_eq!(pnls.len(), 0);
1007 }
1008
1009 #[rstest]
1010 fn test_margin_accessor(
1011 mut margin_account: MarginAccount,
1012 instrument_id_aud_usd_sim: InstrumentId,
1013 ) {
1014 let margin_balance = MarginBalance::new(
1015 Money::from("1000 USD"),
1016 Money::from("500 USD"),
1017 instrument_id_aud_usd_sim,
1018 );
1019
1020 margin_account.update_margin(margin_balance);
1021
1022 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1023 assert!(retrieved.is_some());
1024 let retrieved = retrieved.unwrap();
1025 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1026 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1027 assert_eq!(retrieved.instrument_id, instrument_id_aud_usd_sim);
1028 }
1029
1030 #[rstest]
1031 fn test_clear_margin(
1032 mut margin_account: MarginAccount,
1033 instrument_id_aud_usd_sim: InstrumentId,
1034 ) {
1035 let margin_balance = MarginBalance::new(
1036 Money::from("1000 USD"),
1037 Money::from("500 USD"),
1038 instrument_id_aud_usd_sim,
1039 );
1040
1041 margin_account.update_margin(margin_balance);
1042 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1043
1044 margin_account.clear_margin(instrument_id_aud_usd_sim);
1045 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1046 }
1047}