1#![allow(dead_code)]
31
32use std::{
33 fmt::Display,
34 hash::{Hash, Hasher},
35 ops::{Deref, DerefMut},
36};
37
38use ahash::AHashMap;
39use nautilus_core::correctness::{FAILED, check_positive_decimal};
40use rust_decimal::Decimal;
41use serde::{Deserialize, Serialize};
42
43use crate::{
44 accounts::{Account, base::BaseAccount},
45 enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
46 events::{AccountState, OrderFilled},
47 identifiers::{AccountId, InstrumentId},
48 instruments::{Instrument, InstrumentAny},
49 position::Position,
50 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity, money::MoneyRaw},
51};
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[cfg_attr(
55 feature = "python",
56 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
57)]
58pub struct MarginAccount {
59 pub base: BaseAccount,
60 pub leverages: AHashMap<InstrumentId, Decimal>,
61 pub margins: AHashMap<InstrumentId, MarginBalance>,
62 pub default_leverage: Decimal,
63}
64
65impl MarginAccount {
66 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
68 Self {
69 base: BaseAccount::new(event, calculate_account_state),
70 leverages: AHashMap::new(),
71 margins: AHashMap::new(),
72 default_leverage: Decimal::ONE,
73 }
74 }
75
76 pub fn set_default_leverage(&mut self, leverage: Decimal) {
82 check_positive_decimal(leverage, "leverage").expect(FAILED);
83 self.default_leverage = leverage;
84 }
85
86 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
92 check_positive_decimal(leverage, "leverage").expect(FAILED);
93 self.leverages.insert(instrument_id, leverage);
94 }
95
96 #[must_use]
97 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
98 *self
99 .leverages
100 .get(instrument_id)
101 .unwrap_or(&self.default_leverage)
102 }
103
104 #[must_use]
105 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
106 self.get_leverage(&instrument_id) == Decimal::ONE
107 }
108
109 #[must_use]
110 pub fn is_cash_account(&self) -> bool {
111 self.account_type == AccountType::Cash
112 }
113
114 #[must_use]
115 pub fn is_margin_account(&self) -> bool {
116 self.account_type == AccountType::Margin
117 }
118
119 #[must_use]
120 pub fn initial_margins(&self) -> AHashMap<InstrumentId, Money> {
121 let mut initial_margins: AHashMap<InstrumentId, Money> = AHashMap::new();
122 self.margins.values().for_each(|margin_balance| {
123 initial_margins.insert(margin_balance.instrument_id, margin_balance.initial);
124 });
125 initial_margins
126 }
127
128 #[must_use]
129 pub fn maintenance_margins(&self) -> AHashMap<InstrumentId, Money> {
130 let mut maintenance_margins: AHashMap<InstrumentId, Money> = AHashMap::new();
131 self.margins.values().for_each(|margin_balance| {
132 maintenance_margins.insert(margin_balance.instrument_id, margin_balance.maintenance);
133 });
134 maintenance_margins
135 }
136
137 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
143 let margin_balance = self.margins.get(&instrument_id);
144 if let Some(balance) = margin_balance {
145 let mut new_margin_balance = *balance;
147 new_margin_balance.initial = margin_init;
148 self.margins.insert(instrument_id, new_margin_balance);
149 } else {
150 self.margins.insert(
151 instrument_id,
152 MarginBalance::new(
153 margin_init,
154 Money::new(0.0, margin_init.currency),
155 instrument_id,
156 ),
157 );
158 }
159 self.recalculate_balance(margin_init.currency);
160 }
161
162 #[must_use]
168 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
169 let margin_balance = self.margins.get(&instrument_id);
170 assert!(
171 margin_balance.is_some(),
172 "Cannot get margin_init when no margin_balance"
173 );
174 margin_balance.unwrap().initial
175 }
176
177 pub fn update_maintenance_margin(
183 &mut self,
184 instrument_id: InstrumentId,
185 margin_maintenance: Money,
186 ) {
187 let margin_balance = self.margins.get(&instrument_id);
188 if let Some(balance) = margin_balance {
189 let mut new_margin_balance = *balance;
191 new_margin_balance.maintenance = margin_maintenance;
192 self.margins.insert(instrument_id, new_margin_balance);
193 } else {
194 self.margins.insert(
195 instrument_id,
196 MarginBalance::new(
197 Money::new(0.0, margin_maintenance.currency),
198 margin_maintenance,
199 instrument_id,
200 ),
201 );
202 }
203 self.recalculate_balance(margin_maintenance.currency);
204 }
205
206 #[must_use]
212 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
213 let margin_balance = self.margins.get(&instrument_id);
214 assert!(
215 margin_balance.is_some(),
216 "Cannot get maintenance_margin when no margin_balance"
217 );
218 margin_balance.unwrap().maintenance
219 }
220
221 #[must_use]
223 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
224 self.margins.get(instrument_id).copied()
225 }
226
227 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
229 self.margins
230 .insert(margin_balance.instrument_id, margin_balance);
231 self.recalculate_balance(margin_balance.currency);
232 }
233
234 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
236 if let Some(margin_balance) = self.margins.remove(&instrument_id) {
237 self.recalculate_balance(margin_balance.currency);
238 }
239 }
240
241 pub fn calculate_initial_margin<T: Instrument>(
252 &mut self,
253 instrument: T,
254 quantity: Quantity,
255 price: Price,
256 use_quote_for_inverse: Option<bool>,
257 ) -> anyhow::Result<Money> {
258 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
259 let leverage = self.get_leverage(&instrument.id());
260 if leverage <= Decimal::ZERO {
261 anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
262 }
263 let notional_decimal = notional.as_decimal();
264 let adjusted_notional = notional_decimal / leverage;
265 let margin_decimal = adjusted_notional * instrument.margin_init();
266
267 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
268 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
269 instrument.base_currency().unwrap()
270 } else {
271 instrument.quote_currency()
272 };
273
274 Money::from_decimal(margin_decimal, currency)
275 }
276
277 pub fn calculate_maintenance_margin<T: Instrument>(
288 &mut self,
289 instrument: T,
290 quantity: Quantity,
291 price: Price,
292 use_quote_for_inverse: Option<bool>,
293 ) -> anyhow::Result<Money> {
294 let notional = instrument.calculate_notional_value(quantity, price, use_quote_for_inverse);
295 let leverage = self.get_leverage(&instrument.id());
296 if leverage <= Decimal::ZERO {
297 anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
298 }
299 let notional_decimal = notional.as_decimal();
300 let adjusted_notional = notional_decimal / leverage;
301 let margin_decimal = adjusted_notional * instrument.margin_maint();
302
303 let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false);
304 let currency = if instrument.is_inverse() && !use_quote_for_inverse {
305 instrument.base_currency().unwrap()
306 } else {
307 instrument.quote_currency()
308 };
309
310 Money::from_decimal(margin_decimal, currency)
311 }
312
313 pub fn recalculate_balance(&mut self, currency: Currency) {
320 let current_balance = match self.balances.get(¤cy) {
321 Some(balance) => *balance,
322 None => {
323 let zero = Money::from_raw(0, currency);
326 AccountBalance::new(zero, zero, zero)
327 }
328 };
329
330 let mut total_margin: MoneyRaw = 0;
331 for margin in self.margins.values() {
332 if margin.currency == currency {
333 total_margin = total_margin
334 .checked_add(margin.initial.raw)
335 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
336 .unwrap_or_else(|| {
337 panic!(
338 "Margin calculation overflow for currency {}: total would exceed maximum",
339 currency.code
340 )
341 });
342 }
343 }
344
345 let total_free = if total_margin > current_balance.total.raw {
349 total_margin = current_balance.total.raw.max(0);
350 current_balance.total.raw - total_margin
351 } else {
352 current_balance.total.raw - total_margin
353 };
354
355 let new_balance = AccountBalance::new(
356 current_balance.total,
357 Money::from_raw(total_margin, currency),
358 Money::from_raw(total_free, currency),
359 );
360 self.balances.insert(currency, new_balance);
361 }
362}
363
364impl Deref for MarginAccount {
365 type Target = BaseAccount;
366
367 fn deref(&self) -> &Self::Target {
368 &self.base
369 }
370}
371
372impl DerefMut for MarginAccount {
373 fn deref_mut(&mut self) -> &mut Self::Target {
374 &mut self.base
375 }
376}
377
378impl Account for MarginAccount {
379 fn id(&self) -> AccountId {
380 self.id
381 }
382
383 fn account_type(&self) -> AccountType {
384 self.account_type
385 }
386
387 fn base_currency(&self) -> Option<Currency> {
388 self.base_currency
389 }
390
391 fn is_cash_account(&self) -> bool {
392 self.account_type == AccountType::Cash
393 }
394
395 fn is_margin_account(&self) -> bool {
396 self.account_type == AccountType::Margin
397 }
398
399 fn calculated_account_state(&self) -> bool {
400 false }
402
403 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
404 self.base_balance_total(currency)
405 }
406
407 fn balances_total(&self) -> AHashMap<Currency, Money> {
408 self.base_balances_total()
409 }
410
411 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
412 self.base_balance_free(currency)
413 }
414
415 fn balances_free(&self) -> AHashMap<Currency, Money> {
416 self.base_balances_free()
417 }
418
419 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
420 self.base_balance_locked(currency)
421 }
422
423 fn balances_locked(&self) -> AHashMap<Currency, Money> {
424 self.base_balances_locked()
425 }
426
427 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
428 self.base_balance(currency)
429 }
430
431 fn last_event(&self) -> Option<AccountState> {
432 self.base_last_event()
433 }
434
435 fn events(&self) -> Vec<AccountState> {
436 self.events.clone()
437 }
438
439 fn event_count(&self) -> usize {
440 self.events.len()
441 }
442
443 fn currencies(&self) -> Vec<Currency> {
444 self.balances.keys().copied().collect()
445 }
446
447 fn starting_balances(&self) -> AHashMap<Currency, Money> {
448 self.balances_starting.clone()
449 }
450
451 fn balances(&self) -> AHashMap<Currency, AccountBalance> {
452 self.balances.clone()
453 }
454
455 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
456 self.base_apply(event);
457 Ok(())
458 }
459
460 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
461 self.base.base_purge_account_events(ts_now, lookback_secs);
462 }
463
464 fn calculate_balance_locked(
465 &mut self,
466 instrument: InstrumentAny,
467 side: OrderSide,
468 quantity: Quantity,
469 price: Price,
470 use_quote_for_inverse: Option<bool>,
471 ) -> anyhow::Result<Money> {
472 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
473 }
474
475 fn calculate_pnls(
476 &self,
477 instrument: InstrumentAny,
478 fill: OrderFilled,
479 position: Option<Position>,
480 ) -> anyhow::Result<Vec<Money>> {
481 let mut pnls: Vec<Money> = Vec::new();
482
483 let instrument_class = instrument.instrument_class();
485 if matches!(
486 instrument_class,
487 InstrumentClass::Option
488 | InstrumentClass::OptionSpread
489 | InstrumentClass::BinaryOption
490 | InstrumentClass::Warrant
491 ) {
492 let notional = instrument.calculate_notional_value(fill.last_qty, fill.last_px, None);
493 let pnl = if fill.order_side == OrderSide::Buy {
494 Money::from_raw(-notional.raw, notional.currency)
495 } else {
496 notional
497 };
498 pnls.push(pnl);
499 return Ok(pnls);
500 }
501
502 if let Some(ref pos) = position
504 && pos.quantity.is_positive()
505 && pos.entry != fill.order_side
506 {
507 let pnl_quantity = Quantity::from_raw(
510 fill.last_qty.raw.min(pos.quantity.raw),
511 fill.last_qty.precision,
512 );
513 let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
514 pnls.push(pnl);
515 }
516
517 Ok(pnls)
518 }
519
520 fn calculate_commission(
521 &self,
522 instrument: InstrumentAny,
523 last_qty: Quantity,
524 last_px: Price,
525 liquidity_side: LiquiditySide,
526 use_quote_for_inverse: Option<bool>,
527 ) -> anyhow::Result<Money> {
528 self.base_calculate_commission(
529 instrument,
530 last_qty,
531 last_px,
532 liquidity_side,
533 use_quote_for_inverse,
534 )
535 }
536}
537
538impl PartialEq for MarginAccount {
539 fn eq(&self, other: &Self) -> bool {
540 self.id == other.id
541 }
542}
543
544impl Eq for MarginAccount {}
545
546impl Display for MarginAccount {
547 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
548 write!(
549 f,
550 "MarginAccount(id={}, type={}, base={})",
551 self.id,
552 self.account_type,
553 self.base_currency.map_or_else(
554 || "None".to_string(),
555 |base_currency| format!("{}", base_currency.code)
556 ),
557 )
558 }
559}
560
561impl Hash for MarginAccount {
562 fn hash<H: Hasher>(&self, state: &mut H) {
563 self.id.hash(state);
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use ahash::AHashMap;
570 use nautilus_core::UnixNanos;
571 use rstest::rstest;
572 use rust_decimal::Decimal;
573
574 use crate::{
575 accounts::{Account, MarginAccount, stubs::*},
576 enums::{LiquiditySide, OrderSide, OrderType},
577 events::{AccountState, OrderFilled, account::stubs::*},
578 identifiers::{
579 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
580 VenueOrderId,
581 stubs::{uuid4, *},
582 },
583 instruments::{
584 CryptoPerpetual, CurrencyPair, InstrumentAny,
585 stubs::{binary_option, option_contract_appl, *},
586 },
587 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
588 position::Position,
589 types::{Currency, MarginBalance, Money, Price, Quantity},
590 };
591
592 #[rstest]
593 fn test_display(margin_account: MarginAccount) {
594 assert_eq!(
595 margin_account.to_string(),
596 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
597 );
598 }
599
600 #[rstest]
601 fn test_base_account_properties(
602 margin_account: MarginAccount,
603 margin_account_state: AccountState,
604 ) {
605 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
606 assert_eq!(
607 margin_account.last_event(),
608 Some(margin_account_state.clone())
609 );
610 assert_eq!(margin_account.events(), vec![margin_account_state]);
611 assert_eq!(margin_account.event_count(), 1);
612 assert_eq!(
613 margin_account.balance_total(None),
614 Some(Money::from("1525000 USD"))
615 );
616 assert_eq!(
617 margin_account.balance_free(None),
618 Some(Money::from("1500000 USD"))
619 );
620 assert_eq!(
621 margin_account.balance_locked(None),
622 Some(Money::from("25000 USD"))
623 );
624 let mut balances_total_expected = AHashMap::new();
625 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
626 assert_eq!(margin_account.balances_total(), balances_total_expected);
627 let mut balances_free_expected = AHashMap::new();
628 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
629 assert_eq!(margin_account.balances_free(), balances_free_expected);
630 let mut balances_locked_expected = AHashMap::new();
631 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
632 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
633 }
634
635 #[rstest]
636 fn test_set_default_leverage(mut margin_account: MarginAccount) {
637 assert_eq!(margin_account.default_leverage, Decimal::ONE);
638 margin_account.set_default_leverage(Decimal::from(10));
639 assert_eq!(margin_account.default_leverage, Decimal::from(10));
640 }
641
642 #[rstest]
643 fn test_get_leverage_default_leverage(
644 margin_account: MarginAccount,
645 instrument_id_aud_usd_sim: InstrumentId,
646 ) {
647 assert_eq!(
648 margin_account.get_leverage(&instrument_id_aud_usd_sim),
649 Decimal::ONE
650 );
651 }
652
653 #[rstest]
654 fn test_set_leverage(
655 mut margin_account: MarginAccount,
656 instrument_id_aud_usd_sim: InstrumentId,
657 ) {
658 assert_eq!(margin_account.leverages.len(), 0);
659 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
660 assert_eq!(margin_account.leverages.len(), 1);
661 assert_eq!(
662 margin_account.get_leverage(&instrument_id_aud_usd_sim),
663 Decimal::from(10)
664 );
665 }
666
667 #[rstest]
668 fn test_is_unleveraged_with_leverage_returns_false(
669 mut margin_account: MarginAccount,
670 instrument_id_aud_usd_sim: InstrumentId,
671 ) {
672 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
673 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
674 }
675
676 #[rstest]
677 fn test_is_unleveraged_with_no_leverage_returns_true(
678 mut margin_account: MarginAccount,
679 instrument_id_aud_usd_sim: InstrumentId,
680 ) {
681 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
682 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
683 }
684
685 #[rstest]
686 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
687 margin_account: MarginAccount,
688 instrument_id_aud_usd_sim: InstrumentId,
689 ) {
690 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
691 }
692
693 #[rstest]
694 fn test_update_margin_init(
695 mut margin_account: MarginAccount,
696 instrument_id_aud_usd_sim: InstrumentId,
697 ) {
698 assert_eq!(margin_account.margins.len(), 0);
699 let margin = Money::from("10000 USD");
700 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
701 assert_eq!(
702 margin_account.initial_margin(instrument_id_aud_usd_sim),
703 margin
704 );
705 let margins: Vec<Money> = margin_account
706 .margins
707 .values()
708 .map(|margin_balance| margin_balance.initial)
709 .collect();
710 assert_eq!(margins, vec![margin]);
711 }
712
713 #[rstest]
714 fn test_update_margin_maintenance(
715 mut margin_account: MarginAccount,
716 instrument_id_aud_usd_sim: InstrumentId,
717 ) {
718 let margin = Money::from("10000 USD");
719 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
720 assert_eq!(
721 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
722 margin
723 );
724 let margins: Vec<Money> = margin_account
725 .margins
726 .values()
727 .map(|margin_balance| margin_balance.maintenance)
728 .collect();
729 assert_eq!(margins, vec![margin]);
730 }
731
732 #[rstest]
733 fn test_calculate_margin_init_with_leverage(
734 mut margin_account: MarginAccount,
735 audusd_sim: CurrencyPair,
736 ) {
737 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
738 let result = margin_account
739 .calculate_initial_margin(
740 audusd_sim,
741 Quantity::from(100_000),
742 Price::from("0.8000"),
743 None,
744 )
745 .unwrap();
746 assert_eq!(result, Money::from("48.00 USD"));
747 }
748
749 #[rstest]
750 fn test_calculate_margin_init_with_default_leverage(
751 mut margin_account: MarginAccount,
752 audusd_sim: CurrencyPair,
753 ) {
754 margin_account.set_default_leverage(Decimal::from(10));
755 let result = margin_account
756 .calculate_initial_margin(
757 audusd_sim,
758 Quantity::from(100_000),
759 Price::from("0.8"),
760 None,
761 )
762 .unwrap();
763 assert_eq!(result, Money::from("240.00 USD"));
764 }
765
766 #[rstest]
767 fn test_calculate_margin_init_with_no_leverage_for_inverse(
768 mut margin_account: MarginAccount,
769 xbtusd_bitmex: CryptoPerpetual,
770 ) {
771 let result_use_quote_inverse_true = margin_account
772 .calculate_initial_margin(
773 xbtusd_bitmex,
774 Quantity::from(100_000),
775 Price::from("11493.60"),
776 Some(false),
777 )
778 .unwrap();
779 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
780 let result_use_quote_inverse_false = margin_account
781 .calculate_initial_margin(
782 xbtusd_bitmex,
783 Quantity::from(100_000),
784 Price::from("11493.60"),
785 Some(true),
786 )
787 .unwrap();
788 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
789 }
790
791 #[rstest]
792 fn test_calculate_margin_maintenance_with_no_leverage(
793 mut margin_account: MarginAccount,
794 xbtusd_bitmex: CryptoPerpetual,
795 ) {
796 let result = margin_account
797 .calculate_maintenance_margin(
798 xbtusd_bitmex,
799 Quantity::from(100_000),
800 Price::from("11493.60"),
801 None,
802 )
803 .unwrap();
804 assert_eq!(result, Money::from("0.03045173 BTC"));
805 }
806
807 #[rstest]
808 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
809 mut margin_account: MarginAccount,
810 audusd_sim: CurrencyPair,
811 ) {
812 margin_account.set_default_leverage(Decimal::from(50));
813 let result = margin_account
814 .calculate_maintenance_margin(
815 audusd_sim,
816 Quantity::from(1_000_000),
817 Price::from("1"),
818 None,
819 )
820 .unwrap();
821 assert_eq!(result, Money::from("600.00 USD"));
822 }
823
824 #[rstest]
825 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
826 mut margin_account: MarginAccount,
827 xbtusd_bitmex: CryptoPerpetual,
828 ) {
829 margin_account.set_default_leverage(Decimal::from(10));
830 let result = margin_account
831 .calculate_maintenance_margin(
832 xbtusd_bitmex,
833 Quantity::from(100_000),
834 Price::from("100000.00"),
835 None,
836 )
837 .unwrap();
838 assert_eq!(result, Money::from("0.00035000 BTC"));
839 }
840
841 #[rstest]
842 fn test_calculate_pnls_github_issue_2657() {
843 let account_state = margin_account_state();
845 let account = MarginAccount::new(account_state, false);
846
847 let btcusdt = currency_pair_btcusdt();
849 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
850
851 let fill1 = OrderFilled::new(
853 TraderId::from("TRADER-001"),
854 StrategyId::from("S-001"),
855 btcusdt.id,
856 ClientOrderId::from("O-1"),
857 VenueOrderId::from("V-1"),
858 AccountId::from("SIM-001"),
859 TradeId::from("T-1"),
860 OrderSide::Buy,
861 OrderType::Market,
862 Quantity::from("0.001"),
863 Price::from("50000.00"),
864 btcusdt.quote_currency,
865 LiquiditySide::Taker,
866 uuid4(),
867 UnixNanos::from(1_000_000_000),
868 UnixNanos::default(),
869 false,
870 Some(PositionId::from("P-GITHUB-2657")),
871 None,
872 );
873
874 let position = Position::new(&btcusdt_any, fill1);
875
876 let fill2 = OrderFilled::new(
878 TraderId::from("TRADER-001"),
879 StrategyId::from("S-001"),
880 btcusdt.id,
881 ClientOrderId::from("O-2"),
882 VenueOrderId::from("V-2"),
883 AccountId::from("SIM-001"),
884 TradeId::from("T-2"),
885 OrderSide::Sell,
886 OrderType::Market,
887 Quantity::from("0.002"), Price::from("50075.00"),
889 btcusdt.quote_currency,
890 LiquiditySide::Taker,
891 uuid4(),
892 UnixNanos::from(2_000_000_000),
893 UnixNanos::default(),
894 false,
895 Some(PositionId::from("P-GITHUB-2657")),
896 None,
897 );
898
899 let pnls = account
901 .calculate_pnls(btcusdt_any, fill2, Some(position))
902 .unwrap();
903
904 assert_eq!(pnls.len(), 1);
906
907 let expected_pnl = Money::from("0.075 USDT");
910 assert_eq!(pnls[0], expected_pnl);
911 }
912
913 #[rstest]
914 #[should_panic(expected = "not positive")]
915 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
916 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
917 }
918
919 #[rstest]
920 #[should_panic(expected = "not positive")]
921 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
922 margin_account.set_default_leverage(Decimal::ZERO);
923 }
924
925 #[rstest]
926 #[should_panic(expected = "not positive")]
927 fn test_set_leverage_negative_panics(
928 mut margin_account: MarginAccount,
929 audusd_sim: CurrencyPair,
930 ) {
931 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
932 }
933
934 #[rstest]
935 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
936 use nautilus_core::UnixNanos;
937
938 use crate::{
939 enums::{LiquiditySide, OrderSide, OrderType},
940 events::OrderFilled,
941 identifiers::{
942 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
943 stubs::uuid4,
944 },
945 instruments::InstrumentAny,
946 position::Position,
947 types::{Price, Quantity},
948 };
949
950 let account_state = margin_account_state();
952 let account = MarginAccount::new(account_state, false);
953
954 let btcusdt = currency_pair_btcusdt();
956 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
957
958 let fill1 = OrderFilled::new(
960 TraderId::from("TRADER-001"),
961 StrategyId::from("S-001"),
962 btcusdt.id,
963 ClientOrderId::from("O-1"),
964 VenueOrderId::from("V-1"),
965 AccountId::from("SIM-001"),
966 TradeId::from("T-1"),
967 OrderSide::Buy,
968 OrderType::Market,
969 Quantity::from("1.0"),
970 Price::from("50000.00"),
971 btcusdt.quote_currency,
972 LiquiditySide::Taker,
973 uuid4(),
974 UnixNanos::from(1_000_000_000),
975 UnixNanos::default(),
976 false,
977 Some(PositionId::from("P-123456")),
978 None,
979 );
980
981 let position = Position::new(&btcusdt_any, fill1);
982
983 let fill2 = OrderFilled::new(
985 TraderId::from("TRADER-001"),
986 StrategyId::from("S-001"),
987 btcusdt.id,
988 ClientOrderId::from("O-2"),
989 VenueOrderId::from("V-2"),
990 AccountId::from("SIM-001"),
991 TradeId::from("T-2"),
992 OrderSide::Buy, OrderType::Market,
994 Quantity::from("0.5"),
995 Price::from("51000.00"),
996 btcusdt.quote_currency,
997 LiquiditySide::Taker,
998 uuid4(),
999 UnixNanos::from(2_000_000_000),
1000 UnixNanos::default(),
1001 false,
1002 Some(PositionId::from("P-123456")),
1003 None,
1004 );
1005
1006 let pnls = account
1008 .calculate_pnls(btcusdt_any, fill2, Some(position))
1009 .unwrap();
1010
1011 assert_eq!(pnls.len(), 0);
1013 }
1014
1015 #[rstest]
1016 fn test_margin_accessor(
1017 mut margin_account: MarginAccount,
1018 instrument_id_aud_usd_sim: InstrumentId,
1019 ) {
1020 let margin_balance = MarginBalance::new(
1021 Money::from("1000 USD"),
1022 Money::from("500 USD"),
1023 instrument_id_aud_usd_sim,
1024 );
1025
1026 margin_account.update_margin(margin_balance);
1027
1028 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1029 assert!(retrieved.is_some());
1030 let retrieved = retrieved.unwrap();
1031 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1032 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1033 assert_eq!(retrieved.instrument_id, instrument_id_aud_usd_sim);
1034 }
1035
1036 #[rstest]
1037 fn test_clear_margin(
1038 mut margin_account: MarginAccount,
1039 instrument_id_aud_usd_sim: InstrumentId,
1040 ) {
1041 let margin_balance = MarginBalance::new(
1042 Money::from("1000 USD"),
1043 Money::from("500 USD"),
1044 instrument_id_aud_usd_sim,
1045 );
1046
1047 margin_account.update_margin(margin_balance);
1048 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1049
1050 margin_account.clear_margin(instrument_id_aud_usd_sim);
1051 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1052 }
1053
1054 #[rstest]
1055 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1056 let option = option_contract_appl();
1057 let option_any = InstrumentAny::OptionContract(option);
1058
1059 let order = OrderTestBuilder::new(OrderType::Market)
1060 .instrument_id(option.id)
1061 .side(OrderSide::Buy)
1062 .quantity(Quantity::from("10"))
1063 .build();
1064
1065 let fill = TestOrderEventStubs::filled(
1066 &order,
1067 &option_any,
1068 None,
1069 Some(PositionId::new("P-OPT-001")),
1070 Some(Price::from("5.50")),
1071 None,
1072 None,
1073 None,
1074 None,
1075 Some(AccountId::from("SIM-001")),
1076 );
1077
1078 let pnls = margin_account
1079 .calculate_pnls(option_any, fill.into(), None)
1080 .unwrap();
1081
1082 assert_eq!(pnls.len(), 1);
1085 assert_eq!(pnls[0], Money::from("-55 USD"));
1086 }
1087
1088 #[rstest]
1089 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1090 let option = option_contract_appl();
1091 let option_any = InstrumentAny::OptionContract(option);
1092
1093 let order = OrderTestBuilder::new(OrderType::Market)
1094 .instrument_id(option.id)
1095 .side(OrderSide::Sell)
1096 .quantity(Quantity::from("10"))
1097 .build();
1098
1099 let fill = TestOrderEventStubs::filled(
1100 &order,
1101 &option_any,
1102 None,
1103 Some(PositionId::new("P-OPT-002")),
1104 Some(Price::from("5.50")),
1105 None,
1106 None,
1107 None,
1108 None,
1109 Some(AccountId::from("SIM-001")),
1110 );
1111
1112 let pnls = margin_account
1113 .calculate_pnls(option_any, fill.into(), None)
1114 .unwrap();
1115
1116 assert_eq!(pnls.len(), 1);
1119 assert_eq!(pnls[0], Money::from("55 USD"));
1120 }
1121
1122 #[rstest]
1123 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1124 let binary = binary_option();
1125 let binary_any = InstrumentAny::BinaryOption(binary);
1126
1127 let order = OrderTestBuilder::new(OrderType::Market)
1128 .instrument_id(binary.id)
1129 .side(OrderSide::Buy)
1130 .quantity(Quantity::from("100"))
1131 .build();
1132
1133 let fill = TestOrderEventStubs::filled(
1134 &order,
1135 &binary_any,
1136 None,
1137 Some(PositionId::new("P-BIN-001")),
1138 Some(Price::from("0.65")),
1139 None,
1140 None,
1141 None,
1142 None,
1143 Some(AccountId::from("SIM-001")),
1144 );
1145
1146 let pnls = margin_account
1147 .calculate_pnls(binary_any, fill.into(), None)
1148 .unwrap();
1149
1150 assert_eq!(pnls.len(), 1);
1151 assert!(pnls[0].as_f64() < 0.0);
1152 }
1153}