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::prelude::ToPrimitive;
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},
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, f64>,
49 pub margins: HashMap<InstrumentId, MarginBalance>,
50 pub default_leverage: f64,
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: 1.0,
61 }
62 }
63
64 pub fn set_default_leverage(&mut self, leverage: f64) {
65 self.default_leverage = leverage;
66 }
67
68 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: f64) {
69 self.leverages.insert(instrument_id, leverage);
70 }
71
72 #[must_use]
73 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> f64 {
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) == 1.0
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>(
202 &mut self,
203 instrument: T,
204 quantity: Quantity,
205 price: Price,
206 use_quote_for_inverse: Option<bool>,
207 ) -> Money {
208 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
209 let mut leverage = self.get_leverage(&instrument.id());
210 if leverage == 0.0 {
211 self.leverages
212 .insert(instrument.id(), self.default_leverage);
213 leverage = self.default_leverage;
214 }
215 let adjusted_notional = notional / leverage;
216 let initial_margin_f64 = instrument.margin_init().to_f64().unwrap();
217 let margin = adjusted_notional * initial_margin_f64;
218
219 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
220 if instrument.is_inverse() && !use_quote_for_inverse {
221 Money::new(margin, instrument.base_currency().unwrap())
222 } else {
223 Money::new(margin, instrument.quote_currency())
224 }
225 }
226
227 pub fn calculate_maintenance_margin<T: Instrument>(
233 &mut self,
234 instrument: T,
235 quantity: Quantity,
236 price: Price,
237 use_quote_for_inverse: Option<bool>,
238 ) -> Money {
239 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
240 let mut leverage = self.get_leverage(&instrument.id());
241 if leverage == 0.0 {
242 self.leverages
243 .insert(instrument.id(), self.default_leverage);
244 leverage = self.default_leverage;
245 }
246 let adjusted_notional = notional / leverage;
247 let margin_maint_f64 = instrument.margin_maint().to_f64().unwrap();
248 let margin = adjusted_notional * margin_maint_f64;
249
250 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
251 if instrument.is_inverse() && !use_quote_for_inverse {
252 Money::new(margin, instrument.base_currency().unwrap())
253 } else {
254 Money::new(margin, instrument.quote_currency())
255 }
256 }
257
258 pub fn recalculate_balance(&mut self, currency: Currency) {
266 let current_balance = match self.balances.get(¤cy) {
267 Some(balance) => balance,
268 None => panic!("Cannot recalculate balance when no starting balance"),
269 };
270
271 let mut total_margin = 0;
272 self.margins.values().for_each(|margin| {
274 if margin.currency == currency {
275 total_margin += margin.initial.raw;
276 total_margin += margin.maintenance.raw;
277 }
278 });
279 let total_free = current_balance.total.raw - total_margin;
280 assert!(
282 total_free >= 0,
283 "Cannot recalculate balance when total_free is less than 0.0"
284 );
285 let new_balance = AccountBalance::new(
286 current_balance.total,
287 Money::from_raw(total_margin, currency),
288 Money::from_raw(total_free, currency),
289 );
290 self.balances.insert(currency, new_balance);
291 }
292}
293
294impl Deref for MarginAccount {
295 type Target = BaseAccount;
296
297 fn deref(&self) -> &Self::Target {
298 &self.base
299 }
300}
301
302impl DerefMut for MarginAccount {
303 fn deref_mut(&mut self) -> &mut Self::Target {
304 &mut self.base
305 }
306}
307
308impl Account for MarginAccount {
309 fn id(&self) -> AccountId {
310 self.id
311 }
312
313 fn account_type(&self) -> AccountType {
314 self.account_type
315 }
316
317 fn base_currency(&self) -> Option<Currency> {
318 self.base_currency
319 }
320
321 fn is_cash_account(&self) -> bool {
322 self.account_type == AccountType::Cash
323 }
324
325 fn is_margin_account(&self) -> bool {
326 self.account_type == AccountType::Margin
327 }
328
329 fn calculated_account_state(&self) -> bool {
330 false }
332
333 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
334 self.base_balance_total(currency)
335 }
336
337 fn balances_total(&self) -> HashMap<Currency, Money> {
338 self.base_balances_total()
339 }
340
341 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
342 self.base_balance_free(currency)
343 }
344
345 fn balances_free(&self) -> HashMap<Currency, Money> {
346 self.base_balances_free()
347 }
348
349 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
350 self.base_balance_locked(currency)
351 }
352
353 fn balances_locked(&self) -> HashMap<Currency, Money> {
354 self.base_balances_locked()
355 }
356
357 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
358 self.base_balance(currency)
359 }
360
361 fn last_event(&self) -> Option<AccountState> {
362 self.base_last_event()
363 }
364
365 fn events(&self) -> Vec<AccountState> {
366 self.events.clone()
367 }
368
369 fn event_count(&self) -> usize {
370 self.events.len()
371 }
372
373 fn currencies(&self) -> Vec<Currency> {
374 self.balances.keys().copied().collect()
375 }
376
377 fn starting_balances(&self) -> HashMap<Currency, Money> {
378 self.balances_starting.clone()
379 }
380
381 fn balances(&self) -> HashMap<Currency, AccountBalance> {
382 self.balances.clone()
383 }
384
385 fn apply(&mut self, event: AccountState) {
386 self.base_apply(event);
387 }
388
389 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
390 self.base.base_purge_account_events(ts_now, lookback_secs);
391 }
392
393 fn calculate_balance_locked(
394 &mut self,
395 instrument: InstrumentAny,
396 side: OrderSide,
397 quantity: Quantity,
398 price: Price,
399 use_quote_for_inverse: Option<bool>,
400 ) -> anyhow::Result<Money> {
401 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
402 }
403
404 fn calculate_pnls(
405 &self,
406 _instrument: InstrumentAny, fill: OrderFilled,
408 position: Option<Position>,
409 ) -> anyhow::Result<Vec<Money>> {
410 let mut pnls: Vec<Money> = Vec::new();
411
412 if let Some(ref pos) = position
413 && pos.quantity.is_positive()
414 && pos.entry != fill.order_side
415 {
416 let pnl_quantity = Quantity::from_raw(
419 fill.last_qty.raw.min(pos.quantity.raw),
420 fill.last_qty.precision,
421 );
422 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
423 pnls.push(pnl);
424 }
425
426 Ok(pnls)
427 }
428
429 fn calculate_commission(
430 &self,
431 instrument: InstrumentAny,
432 last_qty: Quantity,
433 last_px: Price,
434 liquidity_side: LiquiditySide,
435 use_quote_for_inverse: Option<bool>,
436 ) -> anyhow::Result<Money> {
437 self.base_calculate_commission(
438 instrument,
439 last_qty,
440 last_px,
441 liquidity_side,
442 use_quote_for_inverse,
443 )
444 }
445}
446
447impl PartialEq for MarginAccount {
448 fn eq(&self, other: &Self) -> bool {
449 self.id == other.id
450 }
451}
452
453impl Eq for MarginAccount {}
454
455impl Display for MarginAccount {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 write!(
458 f,
459 "MarginAccount(id={}, type={}, base={})",
460 self.id,
461 self.account_type,
462 self.base_currency.map_or_else(
463 || "None".to_string(),
464 |base_currency| format!("{}", base_currency.code)
465 ),
466 )
467 }
468}
469
470impl Hash for MarginAccount {
471 fn hash<H: Hasher>(&self, state: &mut H) {
472 self.id.hash(state);
473 }
474}
475
476#[cfg(test)]
480mod tests {
481 use std::collections::HashMap;
482
483 use nautilus_core::UnixNanos;
484 use rstest::rstest;
485
486 use crate::{
487 accounts::{Account, MarginAccount, stubs::*},
488 enums::{LiquiditySide, OrderSide, OrderType},
489 events::{AccountState, OrderFilled, account::stubs::*},
490 identifiers::{
491 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
492 VenueOrderId,
493 stubs::{uuid4, *},
494 },
495 instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny, stubs::*},
496 position::Position,
497 types::{Currency, Money, Price, Quantity},
498 };
499
500 #[rstest]
501 fn test_display(margin_account: MarginAccount) {
502 assert_eq!(
503 margin_account.to_string(),
504 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
505 );
506 }
507
508 #[rstest]
509 fn test_base_account_properties(
510 margin_account: MarginAccount,
511 margin_account_state: AccountState,
512 ) {
513 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
514 assert_eq!(
515 margin_account.last_event(),
516 Some(margin_account_state.clone())
517 );
518 assert_eq!(margin_account.events(), vec![margin_account_state]);
519 assert_eq!(margin_account.event_count(), 1);
520 assert_eq!(
521 margin_account.balance_total(None),
522 Some(Money::from("1525000 USD"))
523 );
524 assert_eq!(
525 margin_account.balance_free(None),
526 Some(Money::from("1500000 USD"))
527 );
528 assert_eq!(
529 margin_account.balance_locked(None),
530 Some(Money::from("25000 USD"))
531 );
532 let mut balances_total_expected = HashMap::new();
533 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
534 assert_eq!(margin_account.balances_total(), balances_total_expected);
535 let mut balances_free_expected = HashMap::new();
536 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
537 assert_eq!(margin_account.balances_free(), balances_free_expected);
538 let mut balances_locked_expected = HashMap::new();
539 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
540 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
541 }
542
543 #[rstest]
544 fn test_set_default_leverage(mut margin_account: MarginAccount) {
545 assert_eq!(margin_account.default_leverage, 1.0);
546 margin_account.set_default_leverage(10.0);
547 assert_eq!(margin_account.default_leverage, 10.0);
548 }
549
550 #[rstest]
551 fn test_get_leverage_default_leverage(
552 margin_account: MarginAccount,
553 instrument_id_aud_usd_sim: InstrumentId,
554 ) {
555 assert_eq!(margin_account.get_leverage(&instrument_id_aud_usd_sim), 1.0);
556 }
557
558 #[rstest]
559 fn test_set_leverage(
560 mut margin_account: MarginAccount,
561 instrument_id_aud_usd_sim: InstrumentId,
562 ) {
563 assert_eq!(margin_account.leverages.len(), 0);
564 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
565 assert_eq!(margin_account.leverages.len(), 1);
566 assert_eq!(
567 margin_account.get_leverage(&instrument_id_aud_usd_sim),
568 10.0
569 );
570 }
571
572 #[rstest]
573 fn test_is_unleveraged_with_leverage_returns_false(
574 mut margin_account: MarginAccount,
575 instrument_id_aud_usd_sim: InstrumentId,
576 ) {
577 margin_account.set_leverage(instrument_id_aud_usd_sim, 10.0);
578 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
579 }
580
581 #[rstest]
582 fn test_is_unleveraged_with_no_leverage_returns_true(
583 mut margin_account: MarginAccount,
584 instrument_id_aud_usd_sim: InstrumentId,
585 ) {
586 margin_account.set_leverage(instrument_id_aud_usd_sim, 1.0);
587 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
588 }
589
590 #[rstest]
591 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
592 margin_account: MarginAccount,
593 instrument_id_aud_usd_sim: InstrumentId,
594 ) {
595 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
596 }
597
598 #[rstest]
599 fn test_update_margin_init(
600 mut margin_account: MarginAccount,
601 instrument_id_aud_usd_sim: InstrumentId,
602 ) {
603 assert_eq!(margin_account.margins.len(), 0);
604 let margin = Money::from("10000 USD");
605 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
606 assert_eq!(
607 margin_account.initial_margin(instrument_id_aud_usd_sim),
608 margin
609 );
610 let margins: Vec<Money> = margin_account
611 .margins
612 .values()
613 .map(|margin_balance| margin_balance.initial)
614 .collect();
615 assert_eq!(margins, vec![margin]);
616 }
617
618 #[rstest]
619 fn test_update_margin_maintenance(
620 mut margin_account: MarginAccount,
621 instrument_id_aud_usd_sim: InstrumentId,
622 ) {
623 let margin = Money::from("10000 USD");
624 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
625 assert_eq!(
626 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
627 margin
628 );
629 let margins: Vec<Money> = margin_account
630 .margins
631 .values()
632 .map(|margin_balance| margin_balance.maintenance)
633 .collect();
634 assert_eq!(margins, vec![margin]);
635 }
636
637 #[rstest]
638 fn test_calculate_margin_init_with_leverage(
639 mut margin_account: MarginAccount,
640 audusd_sim: CurrencyPair,
641 ) {
642 margin_account.set_leverage(audusd_sim.id, 50.0);
643 let result = margin_account.calculate_initial_margin(
644 audusd_sim,
645 Quantity::from(100_000),
646 Price::from("0.8000"),
647 None,
648 );
649 assert_eq!(result, Money::from("48.00 USD"));
650 }
651
652 #[rstest]
653 fn test_calculate_margin_init_with_default_leverage(
654 mut margin_account: MarginAccount,
655 audusd_sim: CurrencyPair,
656 ) {
657 margin_account.set_default_leverage(10.0);
658 let result = margin_account.calculate_initial_margin(
659 audusd_sim,
660 Quantity::from(100_000),
661 Price::from("0.8"),
662 None,
663 );
664 assert_eq!(result, Money::from("240.00 USD"));
665 }
666
667 #[rstest]
668 fn test_calculate_margin_init_with_no_leverage_for_inverse(
669 mut margin_account: MarginAccount,
670 xbtusd_bitmex: CryptoPerpetual,
671 ) {
672 let result_use_quote_inverse_true = margin_account.calculate_initial_margin(
673 xbtusd_bitmex,
674 Quantity::from(100_000),
675 Price::from("11493.60"),
676 Some(false),
677 );
678 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
679 let result_use_quote_inverse_false = margin_account.calculate_initial_margin(
680 xbtusd_bitmex,
681 Quantity::from(100_000),
682 Price::from("11493.60"),
683 Some(true),
684 );
685 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
686 }
687
688 #[rstest]
689 fn test_calculate_margin_maintenance_with_no_leverage(
690 mut margin_account: MarginAccount,
691 xbtusd_bitmex: CryptoPerpetual,
692 ) {
693 let result = margin_account.calculate_maintenance_margin(
694 xbtusd_bitmex,
695 Quantity::from(100_000),
696 Price::from("11493.60"),
697 None,
698 );
699 assert_eq!(result, Money::from("0.03045173 BTC"));
700 }
701
702 #[rstest]
703 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
704 mut margin_account: MarginAccount,
705 audusd_sim: CurrencyPair,
706 ) {
707 margin_account.set_default_leverage(50.0);
708 let result = margin_account.calculate_maintenance_margin(
709 audusd_sim,
710 Quantity::from(1_000_000),
711 Price::from("1"),
712 None,
713 );
714 assert_eq!(result, Money::from("600.00 USD"));
715 }
716
717 #[rstest]
718 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
719 mut margin_account: MarginAccount,
720 xbtusd_bitmex: CryptoPerpetual,
721 ) {
722 margin_account.set_default_leverage(10.0);
723 let result = margin_account.calculate_maintenance_margin(
724 xbtusd_bitmex,
725 Quantity::from(100_000),
726 Price::from("100000.00"),
727 None,
728 );
729 assert_eq!(result, Money::from("0.00035000 BTC"));
730 }
731
732 #[rstest]
733 fn test_calculate_pnls_github_issue_2657() {
734 let account_state = margin_account_state();
736 let account = MarginAccount::new(account_state, false);
737
738 let btcusdt = currency_pair_btcusdt();
740 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
741
742 let fill1 = OrderFilled::new(
744 TraderId::from("TRADER-001"),
745 StrategyId::from("S-001"),
746 btcusdt.id,
747 ClientOrderId::from("O-1"),
748 VenueOrderId::from("V-1"),
749 AccountId::from("SIM-001"),
750 TradeId::from("T-1"),
751 OrderSide::Buy,
752 OrderType::Market,
753 Quantity::from("0.001"),
754 Price::from("50000.00"),
755 btcusdt.quote_currency,
756 LiquiditySide::Taker,
757 uuid4(),
758 UnixNanos::from(1_000_000_000),
759 UnixNanos::default(),
760 false,
761 Some(PositionId::from("P-GITHUB-2657")),
762 None,
763 );
764
765 let position = Position::new(&btcusdt_any, fill1);
766
767 let fill2 = OrderFilled::new(
769 TraderId::from("TRADER-001"),
770 StrategyId::from("S-001"),
771 btcusdt.id,
772 ClientOrderId::from("O-2"),
773 VenueOrderId::from("V-2"),
774 AccountId::from("SIM-001"),
775 TradeId::from("T-2"),
776 OrderSide::Sell,
777 OrderType::Market,
778 Quantity::from("0.002"), Price::from("50075.00"),
780 btcusdt.quote_currency,
781 LiquiditySide::Taker,
782 uuid4(),
783 UnixNanos::from(2_000_000_000),
784 UnixNanos::default(),
785 false,
786 Some(PositionId::from("P-GITHUB-2657")),
787 None,
788 );
789
790 let pnls = account
792 .calculate_pnls(btcusdt_any, fill2, Some(position))
793 .unwrap();
794
795 assert_eq!(pnls.len(), 1);
797
798 let expected_pnl = Money::from("0.075 USDT");
801 assert_eq!(pnls[0], expected_pnl);
802 }
803
804 #[rstest]
805 fn test_calculate_initial_margin_with_zero_leverage_falls_back_to_default(
806 mut margin_account: MarginAccount,
807 audusd_sim: CurrencyPair,
808 ) {
809 margin_account.set_default_leverage(10.0);
811
812 margin_account.set_leverage(audusd_sim.id, 0.0);
814
815 let result = margin_account.calculate_initial_margin(
817 audusd_sim,
818 Quantity::from(100_000),
819 Price::from("0.8"),
820 None,
821 );
822
823 assert_eq!(result, Money::from("240.00 USD"));
826
827 assert_eq!(margin_account.get_leverage(&audusd_sim.id), 10.0);
829 }
830
831 #[rstest]
832 fn test_calculate_maintenance_margin_with_zero_leverage_falls_back_to_default(
833 mut margin_account: MarginAccount,
834 audusd_sim: CurrencyPair,
835 ) {
836 margin_account.set_default_leverage(50.0);
838
839 margin_account.set_leverage(audusd_sim.id, 0.0);
841
842 let result = margin_account.calculate_maintenance_margin(
844 audusd_sim,
845 Quantity::from(1_000_000),
846 Price::from("1"),
847 None,
848 );
849
850 assert_eq!(result, Money::from("600.00 USD"));
853
854 assert_eq!(margin_account.get_leverage(&audusd_sim.id), 50.0);
856 }
857
858 #[rstest]
859 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
860 use nautilus_core::UnixNanos;
861
862 use crate::{
863 enums::{LiquiditySide, OrderSide, OrderType},
864 events::OrderFilled,
865 identifiers::{
866 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
867 stubs::uuid4,
868 },
869 instruments::InstrumentAny,
870 position::Position,
871 types::{Price, Quantity},
872 };
873
874 let account_state = margin_account_state();
876 let account = MarginAccount::new(account_state, false);
877
878 let btcusdt = currency_pair_btcusdt();
880 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
881
882 let fill1 = OrderFilled::new(
884 TraderId::from("TRADER-001"),
885 StrategyId::from("S-001"),
886 btcusdt.id,
887 ClientOrderId::from("O-1"),
888 VenueOrderId::from("V-1"),
889 AccountId::from("SIM-001"),
890 TradeId::from("T-1"),
891 OrderSide::Buy,
892 OrderType::Market,
893 Quantity::from("1.0"),
894 Price::from("50000.00"),
895 btcusdt.quote_currency,
896 LiquiditySide::Taker,
897 uuid4(),
898 UnixNanos::from(1_000_000_000),
899 UnixNanos::default(),
900 false,
901 Some(PositionId::from("P-123456")),
902 None,
903 );
904
905 let position = Position::new(&btcusdt_any, fill1);
906
907 let fill2 = OrderFilled::new(
909 TraderId::from("TRADER-001"),
910 StrategyId::from("S-001"),
911 btcusdt.id,
912 ClientOrderId::from("O-2"),
913 VenueOrderId::from("V-2"),
914 AccountId::from("SIM-001"),
915 TradeId::from("T-2"),
916 OrderSide::Buy, OrderType::Market,
918 Quantity::from("0.5"),
919 Price::from("51000.00"),
920 btcusdt.quote_currency,
921 LiquiditySide::Taker,
922 uuid4(),
923 UnixNanos::from(2_000_000_000),
924 UnixNanos::default(),
925 false,
926 Some(PositionId::from("P-123456")),
927 None,
928 );
929
930 let pnls = account
932 .calculate_pnls(btcusdt_any, fill2, Some(position))
933 .unwrap();
934
935 assert_eq!(pnls.len(), 0);
937 }
938}