1use std::{
31 fmt::Display,
32 hash::{Hash, Hasher},
33 ops::{Deref, DerefMut},
34};
35
36use ahash::AHashMap;
37use indexmap::IndexMap;
38use nautilus_core::correctness::{
39 CorrectnessResult, CorrectnessResultExt, FAILED, check_positive_decimal,
40};
41use rust_decimal::Decimal;
42use serde::{Deserialize, Serialize};
43
44use crate::{
45 accounts::{
46 Account,
47 base::BaseAccount,
48 fill_position_pnl,
49 margin_model::{MarginModel, MarginModelHandle},
50 },
51 enums::{AccountType, OrderSide},
52 events::{AccountState, OrderFilled},
53 identifiers::InstrumentId,
54 instruments::{Instrument, InstrumentAny},
55 position::{Position, amount, notional},
56 types::{
57 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
58 money::{MONEY_RAW_MAX, MONEY_RAW_MIN, MoneyRaw},
59 },
60};
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[cfg_attr(
65 feature = "python",
66 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
67)]
68#[cfg_attr(
69 feature = "python",
70 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
71)]
72pub struct MarginAccount {
73 pub base: BaseAccount,
75 pub leverages: AHashMap<InstrumentId, Decimal>,
77 pub margins: IndexMap<InstrumentId, MarginBalance>,
80 pub account_margins: IndexMap<Currency, MarginBalance>,
84 pub default_leverage: Decimal,
86 #[serde(skip, default = "MarginModelHandle::default")]
87 margin_model: MarginModelHandle,
88}
89
90impl MarginAccount {
91 #[must_use]
93 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
94 let (margins, account_margins) = split_event_margins(&event);
95
96 Self {
97 base: BaseAccount::new(event, calculate_account_state),
98 leverages: AHashMap::new(),
99 margins,
100 account_margins,
101 default_leverage: Decimal::ONE,
102 margin_model: MarginModelHandle::default(),
103 }
104 }
105
106 #[must_use]
107 pub(crate) fn clone_without_events(&self) -> Self {
108 Self {
109 base: self.base.clone_without_events(),
110 leverages: self.leverages.clone(),
111 margins: self.margins.clone(),
112 account_margins: self.account_margins.clone(),
113 default_leverage: self.default_leverage,
114 margin_model: self.margin_model.clone(),
115 }
116 }
117
118 pub fn set_margin_model(&mut self, model: MarginModelHandle) {
119 self.margin_model = model;
120 }
121
122 #[must_use]
123 pub const fn margin_model(&self) -> &MarginModelHandle {
124 &self.margin_model
125 }
126
127 pub fn set_default_leverage(&mut self, leverage: Decimal) {
133 self.try_set_default_leverage(leverage)
134 .expect_display(FAILED);
135 }
136
137 pub fn try_set_default_leverage(&mut self, leverage: Decimal) -> CorrectnessResult<()> {
143 check_positive_decimal(leverage, "leverage")?;
144 self.default_leverage = leverage;
145 Ok(())
146 }
147
148 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
154 self.try_set_leverage(instrument_id, leverage)
155 .expect_display(FAILED);
156 }
157
158 pub fn try_set_leverage(
164 &mut self,
165 instrument_id: InstrumentId,
166 leverage: Decimal,
167 ) -> CorrectnessResult<()> {
168 check_positive_decimal(leverage, "leverage")?;
169 self.leverages.insert(instrument_id, leverage);
170 Ok(())
171 }
172
173 #[must_use]
174 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
175 *self
176 .leverages
177 .get(instrument_id)
178 .unwrap_or(&self.default_leverage)
179 }
180
181 #[must_use]
182 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
183 self.get_leverage(&instrument_id) == Decimal::ONE
184 }
185
186 #[must_use]
187 pub fn is_cash_account(&self) -> bool {
188 self.account_type == AccountType::Cash
189 }
190
191 #[must_use]
192 pub fn is_margin_account(&self) -> bool {
193 self.account_type == AccountType::Margin
194 }
195
196 #[must_use]
197 pub fn initial_margins(&self) -> IndexMap<InstrumentId, Money> {
198 self.margins
199 .values()
200 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.initial)))
201 .collect()
202 }
203
204 #[must_use]
205 pub fn maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
206 self.margins
207 .values()
208 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.maintenance)))
209 .collect()
210 }
211
212 #[must_use]
214 pub fn account_initial_margins(&self) -> IndexMap<Currency, Money> {
215 self.account_margins
216 .values()
217 .map(|margin| (margin.currency, margin.initial))
218 .collect()
219 }
220
221 #[must_use]
223 pub fn account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
224 self.account_margins
225 .values()
226 .map(|margin| (margin.currency, margin.maintenance))
227 .collect()
228 }
229
230 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
232 self.update_margin_component(instrument_id, margin_init, MarginComponent::Initial);
233 }
234
235 pub fn clear_initial_margin(&mut self, instrument_id: InstrumentId) {
237 self.clear_margin_component(instrument_id, MarginComponent::Initial);
238 }
239
240 #[must_use]
246 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
247 let margin_balance = self.margins.get(&instrument_id);
248 assert!(
249 margin_balance.is_some(),
250 "Cannot get margin_init when no margin_balance"
251 );
252 margin_balance.unwrap().initial
253 }
254
255 pub fn update_maintenance_margin(
257 &mut self,
258 instrument_id: InstrumentId,
259 margin_maintenance: Money,
260 ) {
261 self.update_margin_component(
262 instrument_id,
263 margin_maintenance,
264 MarginComponent::Maintenance,
265 );
266 }
267
268 pub fn clear_maintenance_margin(&mut self, instrument_id: InstrumentId) {
270 self.clear_margin_component(instrument_id, MarginComponent::Maintenance);
271 }
272
273 fn update_margin_component(
274 &mut self,
275 instrument_id: InstrumentId,
276 amount: Money,
277 component: MarginComponent,
278 ) {
279 let mut margin_balance = self
280 .margins
281 .get(&instrument_id)
282 .copied()
283 .unwrap_or_else(|| {
284 let zero = Money::zero(amount.currency);
285 MarginBalance::new(zero, zero, Some(instrument_id))
286 });
287 component.set(&mut margin_balance, amount);
288 self.margins.insert(instrument_id, margin_balance);
289
290 self.recalculate_balance(amount.currency);
291 }
292
293 fn clear_margin_component(&mut self, instrument_id: InstrumentId, component: MarginComponent) {
294 let Some(mut margin_balance) = self.margins.get(&instrument_id).copied() else {
295 return;
296 };
297
298 let currency = margin_balance.currency;
299 if component.sibling(&margin_balance).is_zero() {
300 self.margins.shift_remove(&instrument_id);
301 } else {
302 component.set(&mut margin_balance, Money::zero(currency));
303 self.margins.insert(instrument_id, margin_balance);
304 }
305
306 self.recalculate_balance(currency);
307 }
308
309 #[must_use]
315 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
316 let margin_balance = self.margins.get(&instrument_id);
317 assert!(
318 margin_balance.is_some(),
319 "Cannot get maintenance_margin when no margin_balance"
320 );
321 margin_balance.unwrap().maintenance
322 }
323
324 #[must_use]
326 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
327 self.margins.get(instrument_id).copied()
328 }
329
330 #[must_use]
332 pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
333 self.account_margins.get(currency).copied()
334 }
335
336 #[must_use]
338 pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
339 self.account_margins.get(currency).map(|m| m.initial)
340 }
341
342 #[must_use]
344 pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
345 self.account_margins.get(currency).map(|m| m.maintenance)
346 }
347
348 #[must_use]
351 pub fn total_initial_margin(&self, currency: Currency) -> Money {
352 self.total_margin_component(currency, MarginComponent::Initial)
353 }
354
355 #[must_use]
358 pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
359 self.total_margin_component(currency, MarginComponent::Maintenance)
360 }
361
362 fn total_margin_component(&self, currency: Currency, component: MarginComponent) -> Money {
363 let raw = self
364 .margins_in(currency)
365 .fold(0 as MoneyRaw, |raw, margin| {
366 raw.saturating_add(component.of(margin).raw())
367 });
368
369 Money::from_raw(clamp_money_raw(raw), currency)
370 }
371
372 fn margins_in(&self, currency: Currency) -> impl Iterator<Item = &MarginBalance> {
374 self.margins
375 .values()
376 .chain(self.account_margins.values())
377 .filter(move |margin| margin.currency == currency)
378 }
379
380 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
386 match margin_balance.instrument_id {
387 Some(instrument_id) => {
388 self.margins.insert(instrument_id, margin_balance);
389 }
390 None => {
391 self.account_margins
392 .insert(margin_balance.currency, margin_balance);
393 }
394 }
395 self.recalculate_balance(margin_balance.currency);
396 }
397
398 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
400 if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
401 self.recalculate_balance(margin_balance.currency);
402 }
403 }
404
405 pub fn clear_account_margin(&mut self, currency: Currency) {
407 if self.account_margins.shift_remove(¤cy).is_some() {
408 self.recalculate_balance(currency);
409 }
410 }
411
412 pub fn calculate_initial_margin<T: Instrument>(
421 &self,
422 instrument: &T,
423 quantity: Quantity,
424 price: Price,
425 use_quote_for_inverse: Option<bool>,
426 ) -> anyhow::Result<Money> {
427 let leverage = self.get_leverage(&instrument.id());
428 self.margin_model.calculate_initial_margin(
429 instrument,
430 quantity,
431 price,
432 leverage,
433 use_quote_for_inverse,
434 )
435 }
436
437 pub fn calculate_maintenance_margin<T: Instrument>(
445 &self,
446 instrument: &T,
447 quantity: Quantity,
448 price: Price,
449 use_quote_for_inverse: Option<bool>,
450 ) -> anyhow::Result<Money> {
451 let leverage = self.get_leverage(&instrument.id());
452 self.margin_model.calculate_maintenance_margin(
453 instrument,
454 quantity,
455 price,
456 leverage,
457 use_quote_for_inverse,
458 )
459 }
460
461 pub fn recalculate_balance(&mut self, currency: Currency) {
466 let current_balance = if let Some(balance) = self.balances.get(¤cy) {
467 *balance
468 } else {
469 log::debug!("Cannot recalculate balance when no current balance for {currency}");
473 return;
474 };
475
476 let new_balance = self.balance_locking_margins(current_balance.total);
477 self.balances.insert(currency, new_balance);
478 }
479
480 #[must_use]
483 pub fn balance_locking_margins(&self, total: Money) -> AccountBalance {
484 let currency = total.currency;
485
486 let total_margin_raw = self
489 .margins_in(currency)
490 .try_fold(0, |raw: MoneyRaw, margin| {
491 raw.checked_add(margin.initial.raw())?
492 .checked_add(margin.maintenance.raw())
493 });
494
495 let total_margin = total_margin_raw.map_or_else(
496 || {
497 log::error!(
498 "Cannot total {currency} margins: the sum exceeded Money bounds; reserving the full balance"
499 );
500 MONEY_RAW_MAX
501 },
502 clamp_money_raw,
503 );
504
505 let mut total_margin = Money::from_raw(total_margin, currency);
509 if total_margin > total {
510 total_margin = total.max(Money::zero(currency));
511 }
512
513 let total_free = total - total_margin;
514
515 AccountBalance::new(total, total_margin, total_free)
516 }
517}
518
519#[derive(Clone, Copy)]
521enum MarginComponent {
522 Initial,
523 Maintenance,
524}
525
526impl MarginComponent {
527 const fn of(self, margin: &MarginBalance) -> Money {
528 match self {
529 Self::Initial => margin.initial,
530 Self::Maintenance => margin.maintenance,
531 }
532 }
533
534 const fn sibling(self, margin: &MarginBalance) -> Money {
536 match self {
537 Self::Initial => margin.maintenance,
538 Self::Maintenance => margin.initial,
539 }
540 }
541
542 const fn set(self, margin: &mut MarginBalance, amount: Money) {
543 match self {
544 Self::Initial => margin.initial = amount,
545 Self::Maintenance => margin.maintenance = amount,
546 }
547 }
548}
549
550fn split_event_margins(
551 event: &AccountState,
552) -> (
553 IndexMap<InstrumentId, MarginBalance>,
554 IndexMap<Currency, MarginBalance>,
555) {
556 let mut per_instrument: IndexMap<InstrumentId, MarginBalance> = IndexMap::new();
557 let mut per_currency: IndexMap<Currency, MarginBalance> = IndexMap::new();
558
559 for margin in &event.margins {
560 match margin.instrument_id {
561 Some(instrument_id) => {
562 per_instrument.insert(instrument_id, *margin);
563 }
564 None => {
565 per_currency.insert(margin.currency, *margin);
566 }
567 }
568 }
569
570 (per_instrument, per_currency)
571}
572
573#[inline]
574fn clamp_money_raw(raw: MoneyRaw) -> MoneyRaw {
575 raw.clamp(MONEY_RAW_MIN, MONEY_RAW_MAX)
576}
577
578impl Deref for MarginAccount {
579 type Target = BaseAccount;
580
581 fn deref(&self) -> &Self::Target {
582 &self.base
583 }
584}
585
586impl DerefMut for MarginAccount {
587 fn deref_mut(&mut self) -> &mut Self::Target {
588 &mut self.base
589 }
590}
591
592impl Account for MarginAccount {
593 impl_account_base_members!();
594
595 fn is_cash_account(&self) -> bool {
596 self.account_type == AccountType::Cash
597 }
598
599 fn is_margin_account(&self) -> bool {
600 self.account_type == AccountType::Margin
601 }
602
603 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
604 self.check_event_account_id(&event)?;
605
606 let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
607 let (per_instrument, per_currency) = split_event_margins(&event);
608 self.base_apply(event);
609
610 if !skip_margin_routing {
611 self.margins = per_instrument;
612 self.account_margins = per_currency;
613 }
614 Ok(())
615 }
616
617 fn calculate_balance_locked(
618 &self,
619 instrument: &InstrumentAny,
620 side: OrderSide,
621 quantity: Quantity,
622 price: Price,
623 use_quote_for_inverse: Option<bool>,
624 ) -> anyhow::Result<Money> {
625 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
626 }
627
628 fn calculate_pnls(
629 &self,
630 instrument: &InstrumentAny,
631 fill: &OrderFilled,
632 position: Option<Position>,
633 ) -> anyhow::Result<Vec<Money>> {
634 if let Some(commission) = fill.commission {
635 amount(commission)?;
636 }
637
638 let reduces = position.as_ref().is_some_and(|position| {
639 position.quantity.is_positive() && position.entry != fill.order_side
640 });
641
642 let net_pnl = fill_position_pnl(instrument, fill, position.as_ref())?;
643 let mut pnls: Vec<Money> = Vec::new();
644
645 if instrument.instrument_class().is_premium_based() {
647 let currency = if instrument.is_inverse() {
648 instrument
649 .base_currency()
650 .ok_or_else(|| anyhow::anyhow!("inverse instrument has no base currency"))?
651 } else {
652 instrument.cost_currency()
653 };
654
655 let notional = notional(
656 fill.last_qty,
657 fill.last_px,
658 instrument.multiplier(),
659 instrument
660 .instrument_class()
661 .divides_notional_by_price(instrument.is_inverse()),
662 currency,
663 )?;
664
665 let pnl = if fill.order_side == OrderSide::Buy {
666 -notional
667 } else {
668 notional
669 };
670 pnls.push(pnl);
671 return Ok(pnls);
672 }
673
674 if let Some(net_pnl) = net_pnl {
675 let pnl = match fill
677 .commission
678 .filter(|fee| fee.currency == net_pnl.currency)
679 {
680 Some(fee) => net_pnl
681 .checked_add(fee)
682 .ok_or_else(|| anyhow::anyhow!("position gross PnL increment overflow"))?,
683 None => net_pnl,
684 };
685
686 if reduces || !pnl.is_zero() {
687 pnls.push(pnl);
688 }
689 }
690
691 Ok(pnls)
692 }
693}
694
695impl PartialEq for MarginAccount {
696 fn eq(&self, other: &Self) -> bool {
697 self.id == other.id
698 }
699}
700
701impl Eq for MarginAccount {}
702
703impl Display for MarginAccount {
704 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705 write!(
706 f,
707 "MarginAccount(id={}, type={}, base={})",
708 self.id,
709 self.account_type,
710 self.base_currency.map_or_else(
711 || "None".to_string(),
712 |base_currency| format!("{}", base_currency.code)
713 ),
714 )
715 }
716}
717
718impl Hash for MarginAccount {
719 fn hash<H: Hasher>(&self, state: &mut H) {
720 self.id.hash(state);
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use ahash::AHashMap;
727 use indexmap::IndexMap;
728 use nautilus_core::UnixNanos;
729 use rstest::rstest;
730 use rust_decimal::Decimal;
731 use rust_decimal_macros::dec;
732
733 use crate::{
734 accounts::{
735 Account, MarginAccount,
736 margin_model::{MarginModel, MarginModelHandle},
737 stubs::*,
738 },
739 enums::{AccountType, LiquiditySide, OrderSide, OrderType},
740 events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
741 fees::{MakerTakerFeeRates, MakerTakerFeeSchedule},
742 identifiers::{
743 AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
744 stubs::{uuid4, *},
745 },
746 instruments::{
747 CryptoOption, CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
748 stubs::{binary_option, option_contract_appl, *},
749 },
750 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
751 position::Position,
752 types::{
753 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
754 money::{MONEY_RAW_MAX, MONEY_RAW_MIN},
755 },
756 };
757
758 struct CustomMarginModel;
759
760 impl MarginModel for CustomMarginModel {
761 fn name(&self) -> &'static str {
762 "custom"
763 }
764
765 fn calculate_initial_margin(
766 &self,
767 _instrument: &dyn Instrument,
768 _quantity: Quantity,
769 _price: Price,
770 _leverage: Decimal,
771 _use_quote_for_inverse: Option<bool>,
772 ) -> anyhow::Result<Money> {
773 Ok(Money::from("12.34 USD"))
774 }
775
776 fn calculate_maintenance_margin(
777 &self,
778 _instrument: &dyn Instrument,
779 _quantity: Quantity,
780 _price: Price,
781 _leverage: Decimal,
782 _use_quote_for_inverse: Option<bool>,
783 ) -> anyhow::Result<Money> {
784 Ok(Money::from("5.67 USD"))
785 }
786 }
787
788 #[rstest]
789 fn test_display(margin_account: MarginAccount) {
790 assert_eq!(
791 margin_account.to_string(),
792 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
793 );
794 }
795
796 #[rstest]
797 fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
798 assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
799 assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
800 }
801
802 #[rstest]
803 fn test_base_account_properties(
804 margin_account: MarginAccount,
805 margin_account_state: AccountState,
806 ) {
807 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
808 assert_eq!(
809 margin_account.last_event(),
810 Some(margin_account_state.clone())
811 );
812 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
813 assert_eq!(margin_account.event_count(), 1);
814 assert_eq!(
815 margin_account.balance_total(None),
816 Some(Money::from("1525000 USD"))
817 );
818 assert_eq!(
819 margin_account.balance_free(None),
820 Some(Money::from("1500000 USD"))
821 );
822 assert_eq!(
823 margin_account.balance_locked(None),
824 Some(Money::from("25000 USD"))
825 );
826 let mut balances_total_expected = IndexMap::new();
827 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
828 assert_eq!(margin_account.balances_total(), balances_total_expected);
829 let mut balances_free_expected = IndexMap::new();
830 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
831 assert_eq!(margin_account.balances_free(), balances_free_expected);
832 let mut balances_locked_expected = IndexMap::new();
833 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
834 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
835 let margin_balance = margin_account_state.margins[0];
836 let instrument_id = margin_balance
837 .instrument_id
838 .expect("stub margin balance carries a concrete instrument_id");
839 let mut initial_margins_expected = IndexMap::new();
840 initial_margins_expected.insert(instrument_id, margin_balance.initial);
841 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
842 let mut maintenance_margins_expected = IndexMap::new();
843 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
844 assert_eq!(
845 margin_account.maintenance_margins(),
846 maintenance_margins_expected
847 );
848 }
849
850 #[rstest]
851 fn test_set_default_leverage(mut margin_account: MarginAccount) {
852 assert_eq!(margin_account.default_leverage, Decimal::ONE);
853 margin_account.set_default_leverage(Decimal::from(10));
854 assert_eq!(margin_account.default_leverage, Decimal::from(10));
855 }
856
857 #[rstest]
858 fn test_get_leverage_default_leverage(
859 margin_account: MarginAccount,
860 instrument_id_aud_usd_sim: InstrumentId,
861 ) {
862 assert_eq!(
863 margin_account.get_leverage(&instrument_id_aud_usd_sim),
864 Decimal::ONE
865 );
866 }
867
868 #[rstest]
869 fn test_set_leverage(
870 mut margin_account: MarginAccount,
871 instrument_id_aud_usd_sim: InstrumentId,
872 ) {
873 assert_eq!(margin_account.leverages.len(), 0);
874 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
875 assert_eq!(margin_account.leverages.len(), 1);
876 assert_eq!(
877 margin_account.get_leverage(&instrument_id_aud_usd_sim),
878 Decimal::from(10)
879 );
880 }
881
882 #[rstest]
883 #[case(dec!(0))]
884 #[case(dec!(-2.5))]
885 fn test_try_leverage_setters_reject_non_positive_without_mutation(
886 mut margin_account: MarginAccount,
887 instrument_id_aud_usd_sim: InstrumentId,
888 #[case] leverage: Decimal,
889 ) {
890 margin_account
891 .try_set_leverage(instrument_id_aud_usd_sim, dec!(10))
892 .unwrap();
893 margin_account.try_set_default_leverage(dec!(5)).unwrap();
894
895 let default_error = margin_account
896 .try_set_default_leverage(leverage)
897 .unwrap_err();
898 let instrument_error = margin_account
899 .try_set_leverage(instrument_id_aud_usd_sim, leverage)
900 .unwrap_err();
901
902 let expected = format!("invalid Decimal for 'leverage' not positive, was {leverage}");
903 assert_eq!(default_error.to_string(), expected);
904 assert_eq!(instrument_error.to_string(), expected);
905 assert_eq!(margin_account.default_leverage, dec!(5));
906 assert_eq!(
907 margin_account.leverages,
908 AHashMap::from([(instrument_id_aud_usd_sim, dec!(10))])
909 );
910 }
911
912 #[rstest]
913 fn test_is_unleveraged_with_leverage_returns_false(
914 mut margin_account: MarginAccount,
915 instrument_id_aud_usd_sim: InstrumentId,
916 ) {
917 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
918 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
919 }
920
921 #[rstest]
922 fn test_is_unleveraged_with_no_leverage_returns_true(
923 mut margin_account: MarginAccount,
924 instrument_id_aud_usd_sim: InstrumentId,
925 ) {
926 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
927 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
928 }
929
930 #[rstest]
931 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
932 margin_account: MarginAccount,
933 instrument_id_aud_usd_sim: InstrumentId,
934 ) {
935 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
936 }
937
938 #[rstest]
939 fn test_update_margin_init(
940 mut margin_account: MarginAccount,
941 instrument_id_aud_usd_sim: InstrumentId,
942 ) {
943 assert_eq!(margin_account.margins.len(), 1);
944 let margin = Money::from("10000 USD");
945 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
946 assert_eq!(
947 margin_account.initial_margin(instrument_id_aud_usd_sim),
948 margin
949 );
950 assert_eq!(margin_account.margins.len(), 2);
951 assert_eq!(
952 margin_account
953 .margins
954 .get(&instrument_id_aud_usd_sim)
955 .expect("AUD/USD margin should exist")
956 .initial,
957 margin
958 );
959 assert_eq!(
960 margin_account
961 .margins
962 .get(&instrument_id_aud_usd_sim)
963 .expect("AUD/USD margin should exist")
964 .maintenance,
965 Money::zero(margin.currency)
966 );
967 }
968
969 #[rstest]
970 fn test_update_margin_maintenance(
971 mut margin_account: MarginAccount,
972 instrument_id_aud_usd_sim: InstrumentId,
973 ) {
974 let margin = Money::from("10000 USD");
975 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
976 assert_eq!(
977 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
978 margin
979 );
980 assert_eq!(margin_account.margins.len(), 2);
981 assert_eq!(
982 margin_account
983 .margins
984 .get(&instrument_id_aud_usd_sim)
985 .expect("AUD/USD margin should exist")
986 .maintenance,
987 margin
988 );
989 assert_eq!(
990 margin_account
991 .margins
992 .get(&instrument_id_aud_usd_sim)
993 .expect("AUD/USD margin should exist")
994 .initial,
995 Money::zero(margin.currency)
996 );
997 assert_eq!(
999 margin_account.balance_locked(Some(Currency::USD())),
1000 Some(Money::from("35000 USD"))
1001 );
1002 assert_eq!(
1003 margin_account.balance_free(Some(Currency::USD())),
1004 Some(Money::from("1490000 USD"))
1005 );
1006 }
1007
1008 #[rstest]
1009 fn test_clear_initial_margin_preserves_maintenance(
1010 mut margin_account: MarginAccount,
1011 instrument_id_aud_usd_sim: InstrumentId,
1012 ) {
1013 let usd = Currency::USD();
1014 margin_account.update_margin(MarginBalance::new(
1015 Money::from("1000 USD"),
1016 Money::from("500 USD"),
1017 Some(instrument_id_aud_usd_sim),
1018 ));
1019
1020 assert_eq!(
1021 margin_account.balance_locked(Some(usd)),
1022 Some(Money::from("26500 USD"))
1023 );
1024
1025 margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
1026
1027 let margin = margin_account
1028 .margin(&instrument_id_aud_usd_sim)
1029 .expect("margin should retain non-zero maintenance");
1030 assert_eq!(margin.initial, Money::from("0 USD"));
1031 assert_eq!(margin.maintenance, Money::from("500 USD"));
1032 assert_eq!(
1033 margin_account.balance_locked(Some(usd)),
1034 Some(Money::from("25500 USD"))
1035 );
1036 assert_eq!(
1037 margin_account.balance_free(Some(usd)),
1038 Some(Money::from("1499500 USD"))
1039 );
1040 assert_eq!(
1041 margin_account.balance_total(Some(usd)),
1042 Some(Money::from("1525000 USD"))
1043 );
1044 }
1045
1046 #[rstest]
1047 fn test_clear_maintenance_margin_removes_empty_entry(
1048 mut margin_account: MarginAccount,
1049 instrument_id_aud_usd_sim: InstrumentId,
1050 ) {
1051 let usd = Currency::USD();
1052 margin_account.update_margin(MarginBalance::new(
1053 Money::from("0 USD"),
1054 Money::from("500 USD"),
1055 Some(instrument_id_aud_usd_sim),
1056 ));
1057
1058 assert_eq!(
1059 margin_account.balance_locked(Some(usd)),
1060 Some(Money::from("25500 USD"))
1061 );
1062
1063 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1064
1065 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1066 assert_eq!(
1067 margin_account.balance_locked(Some(usd)),
1068 Some(Money::from("25000 USD"))
1069 );
1070 assert_eq!(
1071 margin_account.balance_free(Some(usd)),
1072 Some(Money::from("1500000 USD"))
1073 );
1074 }
1075
1076 #[rstest]
1077 fn test_clear_maintenance_margin_preserves_initial(
1078 mut margin_account: MarginAccount,
1079 instrument_id_aud_usd_sim: InstrumentId,
1080 ) {
1081 let usd = Currency::USD();
1082 margin_account.update_margin(MarginBalance::new(
1083 Money::from("1000 USD"),
1084 Money::from("500 USD"),
1085 Some(instrument_id_aud_usd_sim),
1086 ));
1087
1088 assert_eq!(
1089 margin_account.balance_locked(Some(usd)),
1090 Some(Money::from("26500 USD"))
1091 );
1092
1093 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1094
1095 let margin = margin_account
1096 .margin(&instrument_id_aud_usd_sim)
1097 .expect("margin should retain non-zero initial");
1098 assert_eq!(margin.initial, Money::from("1000 USD"));
1099 assert_eq!(margin.maintenance, Money::from("0 USD"));
1100 assert_eq!(
1101 margin_account.balance_locked(Some(usd)),
1102 Some(Money::from("26000 USD"))
1103 );
1104 assert_eq!(
1105 margin_account.balance_free(Some(usd)),
1106 Some(Money::from("1499000 USD"))
1107 );
1108 }
1109
1110 #[rstest]
1111 fn test_apply_replaces_margin_balances_from_event(
1112 mut margin_account: MarginAccount,
1113 margin_account_state: AccountState,
1114 ) {
1115 let old_instrument_id = margin_account_state.margins[0]
1116 .instrument_id
1117 .expect("stub margin balance carries a concrete instrument_id");
1118 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1119 let event = AccountState::new(
1120 margin_account_state.account_id,
1121 AccountType::Margin,
1122 margin_account_state.balances.clone(),
1123 vec![MarginBalance::new(
1124 Money::from("12500 USD"),
1125 Money::from("25000 USD"),
1126 Some(new_instrument_id),
1127 )],
1128 true,
1129 uuid4(),
1130 1.into(),
1131 1.into(),
1132 margin_account_state.base_currency,
1133 );
1134
1135 margin_account.apply(event).unwrap();
1136
1137 assert_eq!(
1138 margin_account.initial_margin(new_instrument_id),
1139 Money::from("12500 USD")
1140 );
1141 assert_eq!(
1142 margin_account.maintenance_margin(new_instrument_id),
1143 Money::from("25000 USD")
1144 );
1145 assert!(margin_account.margin(&old_instrument_id).is_none());
1146 }
1147
1148 #[rstest]
1149 fn test_account_type_predicates(margin_account: MarginAccount) {
1150 assert!(!margin_account.is_cash_account());
1151 assert!(margin_account.is_margin_account());
1152 assert!(!Account::is_cash_account(&margin_account));
1153 assert!(Account::is_margin_account(&margin_account));
1154 }
1155
1156 #[rstest]
1157 fn test_equality_compares_account_ids(margin_account_state: AccountState) {
1158 let account = MarginAccount::new(margin_account_state.clone(), true);
1159 let same = MarginAccount::new(margin_account_state.clone(), true);
1160 let mut other_state = margin_account_state;
1161 other_state.account_id = AccountId::from("OTHER-001");
1162 let other = MarginAccount::new(other_state, true);
1163
1164 assert_eq!(account, same);
1165 assert_ne!(account, other);
1166 }
1167
1168 #[rstest]
1169 fn test_apply_routes_account_margins_when_event_has_no_balances(
1170 mut margin_account: MarginAccount,
1171 margin_account_state: AccountState,
1172 ) {
1173 let usd = Currency::USD();
1174
1175 let event = AccountState::new(
1176 margin_account_state.account_id,
1177 AccountType::Margin,
1178 vec![],
1179 vec![MarginBalance::new(
1180 Money::from("12500 USD"),
1181 Money::from("25000 USD"),
1182 None,
1183 )],
1184 true,
1185 uuid4(),
1186 1.into(),
1187 1.into(),
1188 margin_account_state.base_currency,
1189 );
1190
1191 margin_account.apply(event).unwrap();
1192
1193 assert_eq!(
1194 margin_account.account_initial_margins(),
1195 IndexMap::from([(usd, Money::from("12500 USD"))])
1196 );
1197 assert_eq!(
1198 margin_account.account_maintenance_margins(),
1199 IndexMap::from([(usd, Money::from("25000 USD"))])
1200 );
1201 }
1202
1203 #[rstest]
1204 fn test_apply_routes_account_margins_by_currency(
1205 mut margin_account: MarginAccount,
1206 margin_account_state: AccountState,
1207 ) {
1208 let usd = Currency::USD();
1209 let event = AccountState::new(
1210 margin_account_state.account_id,
1211 AccountType::Margin,
1212 margin_account_state.balances.clone(),
1213 vec![MarginBalance::new(
1214 Money::from("12500 USD"),
1215 Money::from("25000 USD"),
1216 None,
1217 )],
1218 true,
1219 uuid4(),
1220 1.into(),
1221 1.into(),
1222 margin_account_state.base_currency,
1223 );
1224
1225 margin_account.apply(event).unwrap();
1226
1227 assert!(margin_account.margins.is_empty());
1228 assert_eq!(margin_account.account_margins.len(), 1);
1229 assert_eq!(
1230 margin_account.account_initial_margin(&usd),
1231 Some(Money::from("12500 USD"))
1232 );
1233 assert_eq!(
1234 margin_account.account_maintenance_margin(&usd),
1235 Some(Money::from("25000 USD"))
1236 );
1237 assert_eq!(
1238 margin_account.total_initial_margin(usd),
1239 Money::from("12500 USD")
1240 );
1241 }
1242
1243 #[rstest]
1244 fn test_apply_empty_event_preserves_margin_balances(
1245 mut margin_account: MarginAccount,
1246 margin_account_state: AccountState,
1247 ) {
1248 let instrument_id = margin_account_state.margins[0]
1249 .instrument_id
1250 .expect("stub margin balance carries a concrete instrument_id");
1251 let initial_margin = margin_account.initial_margin(instrument_id);
1252 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1253
1254 let empty_event = AccountState::new(
1255 margin_account_state.account_id,
1256 AccountType::Margin,
1257 vec![],
1258 vec![],
1259 true,
1260 uuid4(),
1261 1.into(),
1262 1.into(),
1263 margin_account_state.base_currency,
1264 );
1265
1266 margin_account.apply(empty_event).unwrap();
1267
1268 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1269 assert_eq!(
1270 margin_account.maintenance_margin(instrument_id),
1271 maintenance_margin
1272 );
1273 assert_eq!(margin_account.event_count(), 2);
1274 }
1275
1276 #[rstest]
1277 fn test_calculate_margin_init_with_leverage(
1278 mut margin_account: MarginAccount,
1279 audusd_sim: CurrencyPair,
1280 ) {
1281 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1282 let result = margin_account
1283 .calculate_initial_margin(
1284 &audusd_sim,
1285 Quantity::from(100_000),
1286 Price::from("0.8000"),
1287 None,
1288 )
1289 .unwrap();
1290 assert_eq!(result, Money::from("48.00 USD"));
1291 }
1292
1293 #[rstest]
1294 fn test_custom_margin_model_through_account(
1295 mut margin_account: MarginAccount,
1296 audusd_sim: CurrencyPair,
1297 ) {
1298 margin_account.set_margin_model(MarginModelHandle::new(CustomMarginModel));
1299
1300 let initial = margin_account
1301 .calculate_initial_margin(
1302 &audusd_sim,
1303 Quantity::from(100_000),
1304 Price::from("0.8000"),
1305 None,
1306 )
1307 .unwrap();
1308 let maintenance = margin_account
1309 .calculate_maintenance_margin(
1310 &audusd_sim,
1311 Quantity::from(100_000),
1312 Price::from("0.8000"),
1313 None,
1314 )
1315 .unwrap();
1316
1317 assert_eq!(margin_account.margin_model().name(), "custom");
1318 assert_eq!(initial, Money::from("12.34 USD"));
1319 assert_eq!(maintenance, Money::from("5.67 USD"));
1320 }
1321
1322 #[rstest]
1323 fn test_calculate_margin_init_with_default_leverage(
1324 mut margin_account: MarginAccount,
1325 audusd_sim: CurrencyPair,
1326 ) {
1327 margin_account.set_default_leverage(Decimal::from(10));
1328 let result = margin_account
1329 .calculate_initial_margin(
1330 &audusd_sim,
1331 Quantity::from(100_000),
1332 Price::from("0.8"),
1333 None,
1334 )
1335 .unwrap();
1336 assert_eq!(result, Money::from("240.00 USD"));
1337 }
1338
1339 #[rstest]
1340 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1341 margin_account: MarginAccount,
1342 btcusd_bybit: CryptoPerpetual,
1343 ) {
1344 let result_use_quote_inverse_true = margin_account
1345 .calculate_initial_margin(
1346 &btcusd_bybit,
1347 Quantity::from(100_000),
1348 Price::from("11493.60"),
1349 Some(false),
1350 )
1351 .unwrap();
1352 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1353 let result_use_quote_inverse_false = margin_account
1354 .calculate_initial_margin(
1355 &btcusd_bybit,
1356 Quantity::from(100_000),
1357 Price::from("11493.60"),
1358 Some(true),
1359 )
1360 .unwrap();
1361 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1362 }
1363
1364 #[rstest]
1365 fn test_calculate_margin_maintenance_with_no_leverage(
1366 margin_account: MarginAccount,
1367 btcusd_bybit: CryptoPerpetual,
1368 ) {
1369 let result = margin_account
1370 .calculate_maintenance_margin(
1371 &btcusd_bybit,
1372 Quantity::from(100_000),
1373 Price::from("11493.60"),
1374 None,
1375 )
1376 .unwrap();
1377 assert_eq!(result, Money::from("0.03045173 BTC"));
1378 }
1379
1380 #[rstest]
1381 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1382 mut margin_account: MarginAccount,
1383 audusd_sim: CurrencyPair,
1384 ) {
1385 margin_account.set_default_leverage(Decimal::from(50));
1386 let result = margin_account
1387 .calculate_maintenance_margin(
1388 &audusd_sim,
1389 Quantity::from(1_000_000),
1390 Price::from("1"),
1391 None,
1392 )
1393 .unwrap();
1394 assert_eq!(result, Money::from("600.00 USD"));
1395 }
1396
1397 #[rstest]
1398 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1399 mut margin_account: MarginAccount,
1400 btcusd_bybit: CryptoPerpetual,
1401 ) {
1402 margin_account.set_default_leverage(Decimal::from(10));
1403 let result = margin_account
1404 .calculate_maintenance_margin(
1405 &btcusd_bybit,
1406 Quantity::from(100_000),
1407 Price::from("100000.00"),
1408 None,
1409 )
1410 .unwrap();
1411 assert_eq!(result, Money::from("0.00035000 BTC"));
1412 }
1413
1414 #[rstest]
1415 fn test_calculate_pnls_github_issue_2657() {
1416 let account_state = margin_account_state();
1418 let account = MarginAccount::new(account_state, false);
1419
1420 let btcusdt = currency_pair_btcusdt();
1422 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1423
1424 let fill1 = OrderFilledSpec::builder()
1426 .instrument_id(btcusdt_any.id())
1427 .client_order_id(ClientOrderId::from("O-1"))
1428 .venue_order_id(VenueOrderId::from("V-1"))
1429 .trade_id(TradeId::from("T-1"))
1430 .last_qty(Quantity::from("0.001"))
1431 .last_px(Price::from("50000.00"))
1432 .currency(btcusdt_any.quote_currency())
1433 .ts_event(UnixNanos::from(1_000_000_000))
1434 .position_id(PositionId::from("P-GITHUB-2657"))
1435 .build();
1436
1437 let position = Position::new(&btcusdt_any, fill1);
1438
1439 let fill2 = OrderFilledSpec::builder()
1441 .instrument_id(btcusdt_any.id())
1442 .client_order_id(ClientOrderId::from("O-2"))
1443 .venue_order_id(VenueOrderId::from("V-2"))
1444 .trade_id(TradeId::from("T-2"))
1445 .order_side(OrderSide::Sell)
1446 .last_qty(Quantity::from("0.002")) .last_px(Price::from("50075.00"))
1448 .currency(btcusdt_any.quote_currency())
1449 .ts_event(UnixNanos::from(2_000_000_000))
1450 .position_id(PositionId::from("P-GITHUB-2657"))
1451 .build();
1452
1453 let pnls = account
1455 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1456 .unwrap();
1457
1458 assert_eq!(pnls.len(), 1);
1460
1461 let expected_pnl = Money::from("0.075 USDT");
1464 assert_eq!(pnls[0], expected_pnl);
1465 }
1466
1467 #[rstest]
1468 #[should_panic(expected = "not positive")]
1469 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1470 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1471 }
1472
1473 #[rstest]
1474 #[should_panic(expected = "not positive")]
1475 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1476 margin_account.set_default_leverage(Decimal::ZERO);
1477 }
1478
1479 #[rstest]
1480 #[should_panic(expected = "not positive")]
1481 fn test_set_leverage_negative_panics(
1482 mut margin_account: MarginAccount,
1483 audusd_sim: CurrencyPair,
1484 ) {
1485 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1486 }
1487
1488 #[rstest]
1489 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1490 use nautilus_core::UnixNanos;
1491
1492 use crate::{
1493 events::order::spec::OrderFilledSpec,
1494 identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1495 instruments::InstrumentAny,
1496 position::Position,
1497 types::{Price, Quantity},
1498 };
1499
1500 let account_state = margin_account_state();
1502 let account = MarginAccount::new(account_state, false);
1503
1504 let btcusdt = currency_pair_btcusdt();
1506 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1507
1508 let fill1 = OrderFilledSpec::builder()
1510 .instrument_id(btcusdt.id)
1511 .client_order_id(ClientOrderId::from("O-1"))
1512 .venue_order_id(VenueOrderId::from("V-1"))
1513 .trade_id(TradeId::from("T-1"))
1514 .last_qty(Quantity::from("1.0"))
1515 .last_px(Price::from("50000.00"))
1516 .currency(btcusdt.quote_currency)
1517 .ts_event(UnixNanos::from(1_000_000_000))
1518 .position_id(PositionId::from("P-123456"))
1519 .build();
1520
1521 let position = Position::new(&btcusdt_any, fill1);
1522
1523 let fill2 = OrderFilledSpec::builder()
1525 .instrument_id(btcusdt.id)
1526 .client_order_id(ClientOrderId::from("O-2"))
1527 .venue_order_id(VenueOrderId::from("V-2"))
1528 .trade_id(TradeId::from("T-2"))
1529 .last_qty(Quantity::from("0.5"))
1530 .last_px(Price::from("51000.00"))
1531 .currency(btcusdt.quote_currency)
1532 .ts_event(UnixNanos::from(2_000_000_000))
1533 .position_id(PositionId::from("P-123456"))
1534 .build();
1535
1536 let pnls = account
1538 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1539 .unwrap();
1540
1541 assert_eq!(pnls.len(), 0);
1543 }
1544
1545 #[rstest]
1546 #[case(OrderSide::Buy, "0.1234", OrderSide::Sell, "0.1235")]
1547 #[case(OrderSide::Sell, "0.1235", OrderSide::Buy, "0.1234")]
1548 fn test_calculate_pnls_preserves_micro_profit(
1549 margin_account: MarginAccount,
1550 #[case] opening_side: OrderSide,
1551 #[case] opening_px: &str,
1552 #[case] closing_side: OrderSide,
1553 #[case] closing_px: &str,
1554 ) {
1555 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1556 let opening = OrderFilledSpec::builder()
1557 .instrument_id(instrument.id())
1558 .position_id(PositionId::from("P-MICRO"))
1559 .order_side(opening_side)
1560 .last_qty(Quantity::from("0.01"))
1561 .last_px(Price::from(opening_px))
1562 .commission(Money::zero(Currency::USD()))
1563 .build();
1564 let mut position = Position::new(&instrument, opening);
1565 let closing = OrderFilledSpec::builder()
1566 .instrument_id(instrument.id())
1567 .position_id(position.id)
1568 .trade_id(TradeId::from("T-CLOSE-MICRO"))
1569 .order_side(closing_side)
1570 .last_qty(Quantity::from("0.01"))
1571 .last_px(Price::from(closing_px))
1572 .commission(Money::zero(Currency::USD()))
1573 .build();
1574
1575 let pnls = margin_account
1576 .calculate_pnls(&instrument, &closing, Some(position.clone()))
1577 .unwrap();
1578 position.try_apply(&closing).unwrap();
1579 let expected = Money::from_decimal_exact(dec!(0.000001), Currency::USD()).unwrap();
1580
1581 assert_eq!(pnls, vec![expected]);
1582 assert_eq!(position.realized_pnl, Some(expected));
1583 }
1584
1585 #[cfg(feature = "high-precision")]
1586 #[rstest]
1587 fn test_calculate_pnls_rejects_opening_product_underflow(margin_account: MarginAccount) {
1588 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
1589 let fill = OrderFilledSpec::builder()
1590 .instrument_id(instrument.id())
1591 .last_qty(Quantity::from("0.0000000000000001"))
1592 .last_px(Price::from("0.0000000000000001"))
1593 .commission(Money::from("1 USD"))
1594 .build();
1595
1596 let result = margin_account.calculate_pnls(&instrument, &fill, None);
1597
1598 assert_eq!(
1599 result.unwrap_err().to_string(),
1600 "position proportional arithmetic underflow"
1601 );
1602 }
1603
1604 #[cfg(feature = "defi")]
1605 #[rstest]
1606 #[case::precision_16("50001.0000000000000000")]
1607 #[case::precision_17("50001.00000000000000000")]
1608 #[case::precision_18("50001.000000000000000000")]
1609 fn test_calculate_pnls_preserves_high_precision_reducing_price(
1610 margin_account: MarginAccount,
1611 #[case] last_px: &str,
1612 ) {
1613 let btcusdt = currency_pair_btcusdt();
1614 let instrument = InstrumentAny::CurrencyPair(btcusdt.clone());
1615 let fill_open = OrderFilledSpec::builder()
1616 .instrument_id(btcusdt.id)
1617 .trade_id(TradeId::from("T-1"))
1618 .last_qty(Quantity::from("1.000000"))
1619 .last_px(Price::from("50000.00"))
1620 .currency(btcusdt.quote_currency)
1621 .position_id(PositionId::from("P-1"))
1622 .build();
1623 let position = Position::new(&instrument, fill_open);
1624 let fill_close = OrderFilledSpec::builder()
1625 .instrument_id(btcusdt.id)
1626 .trade_id(TradeId::from("T-2"))
1627 .order_side(OrderSide::Sell)
1628 .last_qty(Quantity::from("2.000000000000000001"))
1629 .last_px(Price::from(last_px))
1630 .currency(btcusdt.quote_currency)
1631 .position_id(PositionId::from("P-1"))
1632 .build();
1633
1634 let result = margin_account.calculate_pnls(&instrument, &fill_close, Some(position));
1635
1636 assert_eq!(result.unwrap(), vec![Money::from("1 USDT")]);
1637 }
1638
1639 #[rstest]
1640 fn test_margin_accessor(
1641 mut margin_account: MarginAccount,
1642 instrument_id_aud_usd_sim: InstrumentId,
1643 ) {
1644 let margin_balance = MarginBalance::new(
1645 Money::from("1000 USD"),
1646 Money::from("500 USD"),
1647 Some(instrument_id_aud_usd_sim),
1648 );
1649
1650 margin_account.update_margin(margin_balance);
1651
1652 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1653 assert!(retrieved.is_some());
1654 let retrieved = retrieved.unwrap();
1655 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1656 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1657 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1658 }
1659
1660 #[rstest]
1661 fn test_clear_margin(
1662 mut margin_account: MarginAccount,
1663 instrument_id_aud_usd_sim: InstrumentId,
1664 ) {
1665 let usd = Currency::USD();
1666 let margin_balance = MarginBalance::new(
1667 Money::from("1000 USD"),
1668 Money::from("500 USD"),
1669 Some(instrument_id_aud_usd_sim),
1670 );
1671
1672 margin_account.update_margin(margin_balance);
1673 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1674 assert_eq!(
1675 margin_account.balance_locked(Some(usd)),
1676 Some(Money::from("26500 USD"))
1677 );
1678 assert_eq!(
1679 margin_account.balance_free(Some(usd)),
1680 Some(Money::from("1498500 USD"))
1681 );
1682
1683 margin_account.clear_margin(instrument_id_aud_usd_sim);
1684
1685 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1686 assert_eq!(
1687 margin_account.balance_locked(Some(usd)),
1688 Some(Money::from("25000 USD"))
1689 );
1690 assert_eq!(
1691 margin_account.balance_free(Some(usd)),
1692 Some(Money::from("1500000 USD"))
1693 );
1694 assert_eq!(
1695 margin_account.balance_total(Some(usd)),
1696 Some(Money::from("1525000 USD"))
1697 );
1698 }
1699
1700 #[rstest]
1701 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1702 let usd = Currency::USD();
1703 let margin_balance =
1704 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1705
1706 margin_account.update_margin(margin_balance);
1707
1708 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1709 assert_eq!(
1710 margin_account.account_initial_margin(&usd),
1711 Some(Money::from("200 USD"))
1712 );
1713 assert_eq!(
1714 margin_account.account_maintenance_margin(&usd),
1715 Some(Money::from("100 USD"))
1716 );
1717 assert_eq!(
1718 margin_account.balance_locked(Some(usd)),
1719 Some(Money::from("25300 USD"))
1720 );
1721 assert_eq!(
1722 margin_account.balance_free(Some(usd)),
1723 Some(Money::from("1499700 USD"))
1724 );
1725
1726 margin_account.clear_account_margin(usd);
1727
1728 assert!(margin_account.account_margin(&usd).is_none());
1729 assert_eq!(
1730 margin_account.balance_locked(Some(usd)),
1731 Some(Money::from("25000 USD"))
1732 );
1733 assert_eq!(
1734 margin_account.balance_free(Some(usd)),
1735 Some(Money::from("1500000 USD"))
1736 );
1737 }
1738
1739 #[rstest]
1740 #[should_panic(expected = "Cannot get margin_init when no margin_balance")]
1741 fn test_initial_margin_panics_when_absent(
1742 margin_account: MarginAccount,
1743 instrument_id_aud_usd_sim: InstrumentId,
1744 ) {
1745 let _ = margin_account.initial_margin(instrument_id_aud_usd_sim);
1746 }
1747
1748 #[rstest]
1749 #[should_panic(expected = "Cannot get maintenance_margin when no margin_balance")]
1750 fn test_maintenance_margin_panics_when_absent(
1751 margin_account: MarginAccount,
1752 instrument_id_aud_usd_sim: InstrumentId,
1753 ) {
1754 let _ = margin_account.maintenance_margin(instrument_id_aud_usd_sim);
1755 }
1756
1757 fn multi_currency_margin_account(reported: Money) -> MarginAccount {
1760 let state = AccountState::new(
1761 AccountId::from("BYBIT-001"),
1762 AccountType::Margin,
1763 vec![AccountBalance::new(
1764 reported,
1765 Money::zero(reported.currency),
1766 reported,
1767 )],
1768 Vec::new(),
1769 true,
1770 uuid4(),
1771 0.into(),
1772 0.into(),
1773 None,
1774 );
1775 MarginAccount::new(state, true)
1776 }
1777
1778 #[rstest]
1779 fn test_margin_update_leaves_unreported_currency_absent() {
1780 let usdt = Currency::USDT();
1781 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1782 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1783
1784 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1785
1786 assert_eq!(account.balance_total(Some(usdt)), None);
1787 assert_eq!(account.balance_locked(Some(usdt)), None);
1788 assert_eq!(account.balance_free(Some(usdt)), None);
1789 assert_eq!(
1790 account.balances.keys().copied().collect::<Vec<_>>(),
1791 vec![Currency::USD()]
1792 );
1793 assert_eq!(
1794 account.initial_margin(instrument_id),
1795 Money::from("5000 USDT")
1796 );
1797 assert_eq!(
1798 account.balance_free(Some(Currency::USD())),
1799 Some(Money::from("1000000 USD"))
1800 );
1801 }
1802
1803 #[rstest]
1804 fn test_margin_update_locks_reported_currency() {
1805 let usdt = Currency::USDT();
1806 let mut account = multi_currency_margin_account(Money::from("100000 USDT"));
1807 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1808
1809 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1810
1811 assert_eq!(
1812 account.balance_total(Some(usdt)),
1813 Some(Money::from("100000 USDT"))
1814 );
1815 assert_eq!(
1816 account.balance_locked(Some(usdt)),
1817 Some(Money::from("5000 USDT"))
1818 );
1819 assert_eq!(
1820 account.balance_free(Some(usdt)),
1821 Some(Money::from("95000 USDT"))
1822 );
1823 }
1824
1825 #[rstest]
1826 fn test_margin_update_locks_currency_reported_after_the_margin() {
1827 let usdt = Currency::USDT();
1828 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1829 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1830
1831 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1832 account
1833 .apply(AccountState::new(
1834 AccountId::from("BYBIT-001"),
1835 AccountType::Margin,
1836 vec![AccountBalance::new(
1837 Money::from("100000 USDT"),
1838 Money::zero(usdt),
1839 Money::from("100000 USDT"),
1840 )],
1841 Vec::new(),
1842 true,
1843 uuid4(),
1844 0.into(),
1845 0.into(),
1846 None,
1847 ))
1848 .unwrap();
1849 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1850
1851 assert_eq!(
1852 account.balance_locked(Some(usdt)),
1853 Some(Money::from("5000 USDT"))
1854 );
1855 assert_eq!(
1856 account.balance_free(Some(usdt)),
1857 Some(Money::from("95000 USDT"))
1858 );
1859 }
1860
1861 #[rstest]
1862 fn test_recalculate_balance_clamps_when_margin_exceeds_total() {
1863 let usdt = Currency::USDT();
1864 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1865
1866 account.update_initial_margin(
1867 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
1868 Money::from("1500 USDT"),
1869 );
1870
1871 assert_eq!(
1872 account.balance_total(Some(usdt)),
1873 Some(Money::from("1000 USDT"))
1874 );
1875 assert_eq!(
1876 account.balance_locked(Some(usdt)),
1877 Some(Money::from("1000 USDT"))
1878 );
1879 assert_eq!(
1880 account.balance_free(Some(usdt)),
1881 Some(Money::from("0 USDT"))
1882 );
1883 }
1884
1885 #[rstest]
1886 fn test_recalculate_balance_keeps_locked_non_negative_when_total_negative() {
1887 let usd = Currency::USD();
1888 let mut account = multi_currency_margin_account(Money::from("-1000 USD"));
1889 account.balances.insert(
1891 usd,
1892 AccountBalance::new(
1893 Money::from("-1000 USD"),
1894 Money::from("250 USD"),
1895 Money::from("-1250 USD"),
1896 ),
1897 );
1898
1899 account.update_initial_margin(InstrumentId::from("EURUSD.SIM"), Money::from("500 USD"));
1900
1901 assert_eq!(
1902 account.balance_total(Some(usd)),
1903 Some(Money::from("-1000 USD"))
1904 );
1905 assert_eq!(
1906 account.balance_locked(Some(usd)),
1907 Some(Money::from("0 USD"))
1908 );
1909 assert_eq!(
1910 account.balance_free(Some(usd)),
1911 Some(Money::from("-1000 USD"))
1912 );
1913 }
1914
1915 #[rstest]
1916 fn test_total_margin_sums_per_instrument_and_account_wide(
1917 mut margin_account: MarginAccount,
1918 instrument_id_aud_usd_sim: InstrumentId,
1919 ) {
1920 let usd = Currency::USD();
1921 let baseline_initial = margin_account.total_initial_margin(usd);
1922 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1923
1924 margin_account.update_margin(MarginBalance::new(
1925 Money::from("100 USD"),
1926 Money::from("50 USD"),
1927 Some(instrument_id_aud_usd_sim),
1928 ));
1929 margin_account.update_margin(MarginBalance::new(
1930 Money::from("200 USD"),
1931 Money::from("150 USD"),
1932 None,
1933 ));
1934
1935 assert_eq!(
1936 margin_account.total_initial_margin(usd),
1937 baseline_initial + Money::from("300 USD"),
1938 );
1939 assert_eq!(
1940 margin_account.total_maintenance_margin(usd),
1941 baseline_maintenance + Money::from("200 USD"),
1942 );
1943 }
1944
1945 #[rstest]
1946 fn test_recalculate_balance_reserves_full_balance_when_margins_cannot_be_totalled() {
1947 let usdt = Currency::USDT();
1948 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1949 let max = Money::from_raw(MONEY_RAW_MAX, usdt);
1950
1951 for symbol in ["ETHUSDT-PERP.BINANCE", "BTCUSDT-PERP.BINANCE"] {
1953 let instrument_id = InstrumentId::from(symbol);
1954 account.margins.insert(
1955 instrument_id,
1956 MarginBalance::new(max, max, Some(instrument_id)),
1957 );
1958 }
1959
1960 account.recalculate_balance(usdt);
1961
1962 assert_eq!(
1963 account.balance_total(Some(usdt)),
1964 Some(Money::from("1000 USDT"))
1965 );
1966 assert_eq!(
1967 account.balance_locked(Some(usdt)),
1968 Some(Money::from("1000 USDT"))
1969 );
1970 assert_eq!(
1971 account.balance_free(Some(usdt)),
1972 Some(Money::from("0 USDT"))
1973 );
1974 }
1975
1976 #[rstest]
1977 fn test_total_margin_clamps_domain_overflow(
1978 mut margin_account: MarginAccount,
1979 instrument_id_aud_usd_sim: InstrumentId,
1980 ) {
1981 let usd = Currency::USD();
1982 let max = Money::from_raw(MONEY_RAW_MAX, usd);
1983 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1984
1985 margin_account.margins.insert(
1986 instrument_id_aud_usd_sim,
1987 MarginBalance::new(max, max, Some(instrument_id_aud_usd_sim)),
1988 );
1989 margin_account.margins.insert(
1990 other_instrument,
1991 MarginBalance::new(max, max, Some(other_instrument)),
1992 );
1993
1994 assert_eq!(margin_account.total_initial_margin(usd), max);
1995 assert_eq!(margin_account.total_maintenance_margin(usd), max);
1996 }
1997
1998 #[rstest]
1999 fn test_total_margin_clamps_negative_domain_overflow(
2000 mut margin_account: MarginAccount,
2001 instrument_id_aud_usd_sim: InstrumentId,
2002 ) {
2003 let usd = Currency::USD();
2004 let min = Money::from_raw(MONEY_RAW_MIN, usd);
2005 let other_instrument = InstrumentId::from("EUR/USD.SIM");
2006
2007 margin_account.margins.insert(
2008 instrument_id_aud_usd_sim,
2009 MarginBalance::new(min, min, Some(instrument_id_aud_usd_sim)),
2010 );
2011 margin_account.margins.insert(
2012 other_instrument,
2013 MarginBalance::new(min, min, Some(other_instrument)),
2014 );
2015
2016 assert_eq!(margin_account.total_initial_margin(usd), min);
2017 assert_eq!(margin_account.total_maintenance_margin(usd), min);
2018 }
2019
2020 #[rstest]
2021 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
2022 let option = option_contract_appl();
2023 let option_any = InstrumentAny::OptionContract(option.clone());
2024
2025 let order = OrderTestBuilder::new(OrderType::Market)
2026 .instrument_id(option.id)
2027 .side(OrderSide::Buy)
2028 .quantity(Quantity::from("10"))
2029 .build();
2030
2031 let fill = TestOrderEventStubs::filled(
2032 &order,
2033 &option_any,
2034 None,
2035 Some(PositionId::new("P-OPT-001")),
2036 Some(Price::from("5.50")),
2037 None,
2038 None,
2039 None,
2040 None,
2041 Some(AccountId::from("SIM-001")),
2042 );
2043
2044 let fill_owned: crate::events::OrderFilled = fill.into();
2045 let pnls = margin_account
2046 .calculate_pnls(&option_any, &fill_owned, None)
2047 .unwrap();
2048
2049 assert_eq!(pnls.len(), 1);
2052 assert_eq!(pnls[0], Money::from("-55 USD"));
2053 }
2054
2055 #[rstest]
2056 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
2057 let option = option_contract_appl();
2058 let option_any = InstrumentAny::OptionContract(option.clone());
2059
2060 let order = OrderTestBuilder::new(OrderType::Market)
2061 .instrument_id(option.id)
2062 .side(OrderSide::Sell)
2063 .quantity(Quantity::from("10"))
2064 .build();
2065
2066 let fill = TestOrderEventStubs::filled(
2067 &order,
2068 &option_any,
2069 None,
2070 Some(PositionId::new("P-OPT-002")),
2071 Some(Price::from("5.50")),
2072 None,
2073 None,
2074 None,
2075 None,
2076 Some(AccountId::from("SIM-001")),
2077 );
2078
2079 let fill_owned: crate::events::OrderFilled = fill.into();
2080 let pnls = margin_account
2081 .calculate_pnls(&option_any, &fill_owned, None)
2082 .unwrap();
2083
2084 assert_eq!(pnls.len(), 1);
2087 assert_eq!(pnls[0], Money::from("55 USD"));
2088 }
2089
2090 #[rstest]
2091 #[case(OrderSide::Buy, "-0.002 BTC")]
2092 #[case(OrderSide::Sell, "0.002 BTC")]
2093 fn test_calculate_pnls_for_inverse_option_realizes_premium_in_base(
2094 margin_account: MarginAccount,
2095 mut crypto_option_btc_deribit: CryptoOption,
2096 #[case] side: OrderSide,
2097 #[case] expected: &str,
2098 ) {
2099 crypto_option_btc_deribit.is_inverse = true;
2100 crypto_option_btc_deribit.multiplier = Quantity::from("0.01");
2101 let option = InstrumentAny::CryptoOption(crypto_option_btc_deribit);
2102 let order = OrderTestBuilder::new(OrderType::Market)
2103 .instrument_id(option.id())
2104 .side(side)
2105 .quantity(Quantity::from("10.0"))
2106 .build();
2107 let fill = TestOrderEventStubs::filled(
2108 &order,
2109 &option,
2110 None,
2111 Some(PositionId::new("P-OPT-003")),
2112 Some(Price::from("0.020")),
2113 None,
2114 None,
2115 None,
2116 None,
2117 Some(AccountId::from("SIM-001")),
2118 );
2119
2120 let pnls = margin_account
2121 .calculate_pnls(&option, &fill.into(), None)
2122 .unwrap();
2123
2124 assert_eq!(pnls, vec![Money::from(expected)]);
2125 }
2126
2127 #[rstest]
2128 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
2129 let binary = binary_option();
2130 let binary_any = InstrumentAny::BinaryOption(binary);
2131
2132 let order = OrderTestBuilder::new(OrderType::Market)
2133 .instrument_id(binary_any.id())
2134 .side(OrderSide::Buy)
2135 .quantity(Quantity::from("100"))
2136 .build();
2137
2138 let fill = TestOrderEventStubs::filled(
2139 &order,
2140 &binary_any,
2141 None,
2142 Some(PositionId::new("P-BIN-001")),
2143 Some(Price::from("0.65")),
2144 None,
2145 None,
2146 None,
2147 None,
2148 Some(AccountId::from("SIM-001")),
2149 );
2150
2151 let fill_owned: crate::events::OrderFilled = fill.into();
2152 let pnls = margin_account
2153 .calculate_pnls(&binary_any, &fill_owned, None)
2154 .unwrap();
2155
2156 assert_eq!(pnls.len(), 1);
2157 assert!(pnls[0].as_f64() < 0.0);
2158 }
2159
2160 #[rstest]
2161 fn test_commission_explicit_schedule_override(
2162 margin_account: MarginAccount,
2163 currency_pair_btcusdt: CurrencyPair,
2164 ) {
2165 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.002), dec!(0.002));
2166 schedule.set_override(
2167 currency_pair_btcusdt.id,
2168 MakerTakerFeeRates::new(dec!(0.0005), dec!(0.0005)),
2169 );
2170 let fee_rates = schedule.rates_for(currency_pair_btcusdt.id);
2171 let result = margin_account
2172 .calculate_commission(
2173 ¤cy_pair_btcusdt.into_any(),
2174 Quantity::from("1"),
2175 Price::from("50000.00"),
2176 LiquiditySide::Maker,
2177 fee_rates,
2178 None,
2179 )
2180 .unwrap();
2181 assert_eq!(result, Money::from("25 USDT"));
2184 }
2185
2186 #[rstest]
2187 fn test_commission_shared_schedule_mutation(
2188 margin_account: MarginAccount,
2189 currency_pair_btcusdt: CurrencyPair,
2190 ) {
2191 let instrument_id = currency_pair_btcusdt.id;
2192 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.001), dec!(0.001));
2193 let before = margin_account
2194 .calculate_commission(
2195 ¤cy_pair_btcusdt.clone().into_any(),
2196 Quantity::from("2"),
2197 Price::from("50000.00"),
2198 LiquiditySide::Taker,
2199 schedule.rates_for(instrument_id),
2200 None,
2201 )
2202 .unwrap();
2203 assert_eq!(before, Money::from("100 USDT"));
2204
2205 schedule.default = MakerTakerFeeRates::new(dec!(0.002), dec!(0.002));
2206 let after = margin_account
2207 .calculate_commission(
2208 ¤cy_pair_btcusdt.into_any(),
2209 Quantity::from("2"),
2210 Price::from("50000.00"),
2211 LiquiditySide::Taker,
2212 schedule.rates_for(instrument_id),
2213 None,
2214 )
2215 .unwrap();
2216 assert_eq!(after, Money::from("200 USDT"));
2217 }
2218}