1#![allow(dead_code)]
31
32use std::{
33 fmt::Display,
34 hash::{Hash, Hasher},
35 ops::{Deref, DerefMut},
36};
37
38use ahash::AHashMap;
39use indexmap::IndexMap;
40use nautilus_core::correctness::{CorrectnessResultExt, FAILED, check_positive_decimal};
41use rust_decimal::Decimal;
42use serde::{Deserialize, Serialize};
43
44use crate::{
45 accounts::{
46 Account,
47 base::BaseAccount,
48 margin_model::{MarginModel, MarginModelHandle},
49 },
50 enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
51 events::{AccountState, OrderFilled},
52 identifiers::{AccountId, InstrumentId},
53 instruments::{Instrument, InstrumentAny},
54 position::Position,
55 types::{
56 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
57 money::{MONEY_RAW_MAX, MONEY_RAW_MIN, MoneyRaw},
58 },
59};
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[cfg_attr(
63 feature = "python",
64 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
65)]
66#[cfg_attr(
67 feature = "python",
68 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
69)]
70pub struct MarginAccount {
71 pub base: BaseAccount,
72 pub leverages: AHashMap<InstrumentId, Decimal>,
73 pub margins: IndexMap<InstrumentId, MarginBalance>,
76 pub account_margins: IndexMap<Currency, MarginBalance>,
80 pub default_leverage: Decimal,
81 #[serde(skip, default = "MarginModelHandle::default")]
82 margin_model: MarginModelHandle,
83}
84
85fn split_event_margins(
86 event: &AccountState,
87) -> (
88 IndexMap<InstrumentId, MarginBalance>,
89 IndexMap<Currency, MarginBalance>,
90) {
91 let mut per_instrument: IndexMap<InstrumentId, MarginBalance> = IndexMap::new();
92 let mut per_currency: IndexMap<Currency, MarginBalance> = IndexMap::new();
93
94 for margin in &event.margins {
95 match margin.instrument_id {
96 Some(instrument_id) => {
97 per_instrument.insert(instrument_id, *margin);
98 }
99 None => {
100 per_currency.insert(margin.currency, *margin);
101 }
102 }
103 }
104 (per_instrument, per_currency)
105}
106
107impl MarginAccount {
108 #[must_use]
110 pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
111 let (margins, account_margins) = split_event_margins(&event);
112
113 Self {
114 base: BaseAccount::new(event, calculate_account_state),
115 leverages: AHashMap::new(),
116 margins,
117 account_margins,
118 default_leverage: Decimal::ONE,
119 margin_model: MarginModelHandle::default(),
120 }
121 }
122
123 #[must_use]
124 pub(crate) fn clone_without_events(&self) -> Self {
125 Self {
126 base: self.base.clone_without_events(),
127 leverages: self.leverages.clone(),
128 margins: self.margins.clone(),
129 account_margins: self.account_margins.clone(),
130 default_leverage: self.default_leverage,
131 margin_model: self.margin_model.clone(),
132 }
133 }
134
135 pub fn set_margin_model(&mut self, model: MarginModelHandle) {
136 self.margin_model = model;
137 }
138
139 #[must_use]
140 pub const fn margin_model(&self) -> &MarginModelHandle {
141 &self.margin_model
142 }
143
144 pub fn set_default_leverage(&mut self, leverage: Decimal) {
150 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
151 self.default_leverage = leverage;
152 }
153
154 pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
160 check_positive_decimal(leverage, "leverage").expect_display(FAILED);
161 self.leverages.insert(instrument_id, leverage);
162 }
163
164 #[must_use]
165 pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
166 *self
167 .leverages
168 .get(instrument_id)
169 .unwrap_or(&self.default_leverage)
170 }
171
172 #[must_use]
173 pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
174 self.get_leverage(&instrument_id) == Decimal::ONE
175 }
176
177 #[must_use]
178 pub fn is_cash_account(&self) -> bool {
179 self.account_type == AccountType::Cash
180 }
181
182 #[must_use]
183 pub fn is_margin_account(&self) -> bool {
184 self.account_type == AccountType::Margin
185 }
186
187 #[must_use]
188 pub fn initial_margins(&self) -> IndexMap<InstrumentId, Money> {
189 self.margins
190 .values()
191 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.initial)))
192 .collect()
193 }
194
195 #[must_use]
196 pub fn maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
197 self.margins
198 .values()
199 .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.maintenance)))
200 .collect()
201 }
202
203 #[must_use]
205 pub fn account_initial_margins(&self) -> IndexMap<Currency, Money> {
206 self.account_margins
207 .values()
208 .map(|margin| (margin.currency, margin.initial))
209 .collect()
210 }
211
212 #[must_use]
214 pub fn account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
215 self.account_margins
216 .values()
217 .map(|margin| (margin.currency, margin.maintenance))
218 .collect()
219 }
220
221 pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
223 let margin_balance = self.margins.get(&instrument_id);
224 if let Some(balance) = margin_balance {
225 let mut new_margin_balance = *balance;
227 new_margin_balance.initial = margin_init;
228 self.margins.insert(instrument_id, new_margin_balance);
229 } else {
230 self.margins.insert(
231 instrument_id,
232 MarginBalance::new(
233 margin_init,
234 Money::zero(margin_init.currency),
235 Some(instrument_id),
236 ),
237 );
238 }
239 self.recalculate_balance(margin_init.currency);
240 }
241
242 pub fn clear_initial_margin(&mut self, instrument_id: InstrumentId) {
244 let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
245 return;
246 };
247
248 if margin_balance.maintenance.is_zero() {
249 self.margins.shift_remove(&instrument_id);
250 } else {
251 let mut new_margin_balance = margin_balance;
252 new_margin_balance.initial = Money::zero(margin_balance.currency);
253 self.margins.insert(instrument_id, new_margin_balance);
254 }
255
256 self.recalculate_balance(margin_balance.currency);
257 }
258
259 #[must_use]
265 pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
266 let margin_balance = self.margins.get(&instrument_id);
267 assert!(
268 margin_balance.is_some(),
269 "Cannot get margin_init when no margin_balance"
270 );
271 margin_balance.unwrap().initial
272 }
273
274 pub fn update_maintenance_margin(
276 &mut self,
277 instrument_id: InstrumentId,
278 margin_maintenance: Money,
279 ) {
280 let margin_balance = self.margins.get(&instrument_id);
281 if let Some(balance) = margin_balance {
282 let mut new_margin_balance = *balance;
284 new_margin_balance.maintenance = margin_maintenance;
285 self.margins.insert(instrument_id, new_margin_balance);
286 } else {
287 self.margins.insert(
288 instrument_id,
289 MarginBalance::new(
290 Money::zero(margin_maintenance.currency),
291 margin_maintenance,
292 Some(instrument_id),
293 ),
294 );
295 }
296 self.recalculate_balance(margin_maintenance.currency);
297 }
298
299 pub fn clear_maintenance_margin(&mut self, instrument_id: InstrumentId) {
301 let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
302 return;
303 };
304
305 if margin_balance.initial.is_zero() {
306 self.margins.shift_remove(&instrument_id);
307 } else {
308 let mut new_margin_balance = margin_balance;
309 new_margin_balance.maintenance = Money::zero(margin_balance.currency);
310 self.margins.insert(instrument_id, new_margin_balance);
311 }
312
313 self.recalculate_balance(margin_balance.currency);
314 }
315
316 #[must_use]
322 pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
323 let margin_balance = self.margins.get(&instrument_id);
324 assert!(
325 margin_balance.is_some(),
326 "Cannot get maintenance_margin when no margin_balance"
327 );
328 margin_balance.unwrap().maintenance
329 }
330
331 #[must_use]
333 pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
334 self.margins.get(instrument_id).copied()
335 }
336
337 #[must_use]
339 pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
340 self.account_margins.get(currency).copied()
341 }
342
343 #[must_use]
345 pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
346 self.account_margins.get(currency).map(|m| m.initial)
347 }
348
349 #[must_use]
351 pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
352 self.account_margins.get(currency).map(|m| m.maintenance)
353 }
354
355 #[must_use]
358 pub fn total_initial_margin(&self, currency: Currency) -> Money {
359 let mut raw: MoneyRaw = 0;
360
361 for margin in self.margins.values() {
362 if margin.currency == currency {
363 raw = raw.saturating_add(margin.initial.raw);
364 }
365 }
366
367 for margin in self.account_margins.values() {
368 if margin.currency == currency {
369 raw = raw.saturating_add(margin.initial.raw);
370 }
371 }
372
373 Money::from_raw(clamp_money_raw(raw), currency)
374 }
375
376 #[must_use]
379 pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
380 let mut raw: MoneyRaw = 0;
381
382 for margin in self.margins.values() {
383 if margin.currency == currency {
384 raw = raw.saturating_add(margin.maintenance.raw);
385 }
386 }
387
388 for margin in self.account_margins.values() {
389 if margin.currency == currency {
390 raw = raw.saturating_add(margin.maintenance.raw);
391 }
392 }
393
394 Money::from_raw(clamp_money_raw(raw), currency)
395 }
396
397 pub fn update_margin(&mut self, margin_balance: MarginBalance) {
403 match margin_balance.instrument_id {
404 Some(instrument_id) => {
405 self.margins.insert(instrument_id, margin_balance);
406 }
407 None => {
408 self.account_margins
409 .insert(margin_balance.currency, margin_balance);
410 }
411 }
412 self.recalculate_balance(margin_balance.currency);
413 }
414
415 pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
417 if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
418 self.recalculate_balance(margin_balance.currency);
419 }
420 }
421
422 pub fn clear_account_margin(&mut self, currency: Currency) {
424 if self.account_margins.shift_remove(¤cy).is_some() {
425 self.recalculate_balance(currency);
426 }
427 }
428
429 pub fn calculate_initial_margin<T: Instrument>(
438 &mut self,
439 instrument: &T,
440 quantity: Quantity,
441 price: Price,
442 use_quote_for_inverse: Option<bool>,
443 ) -> anyhow::Result<Money> {
444 let leverage = self.get_leverage(&instrument.id());
445 self.margin_model.calculate_initial_margin(
446 instrument,
447 quantity,
448 price,
449 leverage,
450 use_quote_for_inverse,
451 )
452 }
453
454 pub fn calculate_maintenance_margin<T: Instrument>(
462 &mut self,
463 instrument: &T,
464 quantity: Quantity,
465 price: Price,
466 use_quote_for_inverse: Option<bool>,
467 ) -> anyhow::Result<Money> {
468 let leverage = self.get_leverage(&instrument.id());
469 self.margin_model.calculate_maintenance_margin(
470 instrument,
471 quantity,
472 price,
473 leverage,
474 use_quote_for_inverse,
475 )
476 }
477
478 pub fn recalculate_balance(&mut self, currency: Currency) {
485 let current_balance = if let Some(balance) = self.balances.get(¤cy) {
486 *balance
487 } else {
488 log::debug!("Cannot recalculate balance when no current balance for {currency}");
492 return;
493 };
494
495 let mut total_margin: MoneyRaw = 0;
496
497 let accumulate = |raw: MoneyRaw, margin: &MarginBalance| -> MoneyRaw {
498 raw.checked_add(margin.initial.raw)
499 .and_then(|sum| sum.checked_add(margin.maintenance.raw))
500 .unwrap_or_else(|| {
501 panic!(
502 "Margin calculation overflow for currency {}: total would exceed maximum",
503 currency.code
504 )
505 })
506 };
507
508 for margin in self.margins.values() {
509 if margin.currency == currency {
510 total_margin = accumulate(total_margin, margin);
511 }
512 }
513
514 for margin in self.account_margins.values() {
515 if margin.currency == currency {
516 total_margin = accumulate(total_margin, margin);
517 }
518 }
519
520 let total_free = if total_margin > current_balance.total.raw {
524 total_margin = current_balance.total.raw.max(0);
525 current_balance.total.raw - total_margin
526 } else {
527 current_balance.total.raw - total_margin
528 };
529
530 let new_balance = AccountBalance::new(
531 current_balance.total,
532 Money::from_raw(total_margin, currency),
533 Money::from_raw(total_free, currency),
534 );
535 self.balances.insert(currency, new_balance);
536 }
537}
538
539#[inline]
540fn clamp_money_raw(raw: MoneyRaw) -> MoneyRaw {
541 raw.clamp(MONEY_RAW_MIN, MONEY_RAW_MAX)
542}
543
544impl Deref for MarginAccount {
545 type Target = BaseAccount;
546
547 fn deref(&self) -> &Self::Target {
548 &self.base
549 }
550}
551
552impl DerefMut for MarginAccount {
553 fn deref_mut(&mut self) -> &mut Self::Target {
554 &mut self.base
555 }
556}
557
558impl Account for MarginAccount {
559 fn id(&self) -> AccountId {
560 self.id
561 }
562
563 fn account_type(&self) -> AccountType {
564 self.account_type
565 }
566
567 fn base_currency(&self) -> Option<Currency> {
568 self.base_currency
569 }
570
571 fn is_cash_account(&self) -> bool {
572 self.account_type == AccountType::Cash
573 }
574
575 fn is_margin_account(&self) -> bool {
576 self.account_type == AccountType::Margin
577 }
578
579 fn calculated_account_state(&self) -> bool {
580 self.calculate_account_state
581 }
582
583 fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
584 self.base_balance_total(currency)
585 }
586
587 fn balances_total(&self) -> IndexMap<Currency, Money> {
588 self.base_balances_total()
589 }
590
591 fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
592 self.base_balance_free(currency)
593 }
594
595 fn balances_free(&self) -> IndexMap<Currency, Money> {
596 self.base_balances_free()
597 }
598
599 fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
600 self.base_balance_locked(currency)
601 }
602
603 fn balances_locked(&self) -> IndexMap<Currency, Money> {
604 self.base_balances_locked()
605 }
606
607 fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
608 self.base_balance(currency)
609 }
610
611 fn last_event(&self) -> Option<AccountState> {
612 self.base_last_event()
613 }
614
615 fn events(&self) -> Vec<AccountState> {
616 self.events.clone()
617 }
618
619 fn event_count(&self) -> usize {
620 self.events.len()
621 }
622
623 fn currencies(&self) -> Vec<Currency> {
624 self.balances.keys().copied().collect()
625 }
626
627 fn starting_balances(&self) -> IndexMap<Currency, Money> {
628 self.balances_starting.clone()
629 }
630
631 fn balances(&self) -> IndexMap<Currency, AccountBalance> {
632 self.balances.clone()
633 }
634
635 fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
636 let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
637 let (per_instrument, per_currency) = split_event_margins(&event);
638 self.base_apply(event);
639
640 if !skip_margin_routing {
641 self.margins = per_instrument;
642 self.account_margins = per_currency;
643 }
644 Ok(())
645 }
646
647 fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
648 self.base.base_purge_account_events(ts_now, lookback_secs);
649 }
650
651 fn calculate_balance_locked(
652 &mut self,
653 instrument: &InstrumentAny,
654 side: OrderSide,
655 quantity: Quantity,
656 price: Price,
657 use_quote_for_inverse: Option<bool>,
658 ) -> anyhow::Result<Money> {
659 self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
660 }
661
662 fn calculate_pnls(
663 &self,
664 instrument: &InstrumentAny,
665 fill: &OrderFilled,
666 position: Option<Position>,
667 ) -> anyhow::Result<Vec<Money>> {
668 let mut pnls: Vec<Money> = Vec::new();
669
670 let instrument_class = instrument.instrument_class();
672
673 if matches!(
674 instrument_class,
675 InstrumentClass::Option
676 | InstrumentClass::OptionSpread
677 | InstrumentClass::BinaryOption
678 | InstrumentClass::Warrant
679 ) {
680 let notional =
681 instrument.try_calculate_notional_value(fill.last_qty, fill.last_px, None)?;
682 let pnl = if fill.order_side == OrderSide::Buy {
683 Money::from_raw(-notional.raw, notional.currency)
684 } else {
685 notional
686 };
687 pnls.push(pnl);
688 return Ok(pnls);
689 }
690
691 if let Some(ref pos) = position
693 && pos.quantity.is_positive()
694 && pos.entry != fill.order_side
695 {
696 let pnl_quantity = Quantity::from_raw(
699 fill.last_qty.raw.min(pos.quantity.raw),
700 fill.last_qty.precision,
701 );
702 let pnl =
703 pos.try_calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity)?;
704 pnls.push(pnl);
705 }
706
707 Ok(pnls)
708 }
709
710 fn calculate_commission(
711 &self,
712 instrument: &InstrumentAny,
713 last_qty: Quantity,
714 last_px: Price,
715 liquidity_side: LiquiditySide,
716 use_quote_for_inverse: Option<bool>,
717 ) -> anyhow::Result<Money> {
718 self.base_calculate_commission(
719 instrument,
720 last_qty,
721 last_px,
722 liquidity_side,
723 use_quote_for_inverse,
724 )
725 }
726}
727
728impl PartialEq for MarginAccount {
729 fn eq(&self, other: &Self) -> bool {
730 self.id == other.id
731 }
732}
733
734impl Eq for MarginAccount {}
735
736impl Display for MarginAccount {
737 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
738 write!(
739 f,
740 "MarginAccount(id={}, type={}, base={})",
741 self.id,
742 self.account_type,
743 self.base_currency.map_or_else(
744 || "None".to_string(),
745 |base_currency| format!("{}", base_currency.code)
746 ),
747 )
748 }
749}
750
751impl Hash for MarginAccount {
752 fn hash<H: Hasher>(&self, state: &mut H) {
753 self.id.hash(state);
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use indexmap::IndexMap;
760 use nautilus_core::UnixNanos;
761 use rstest::rstest;
762 use rust_decimal::Decimal;
763
764 use crate::{
765 accounts::{
766 Account, MarginAccount,
767 margin_model::{MarginModel, MarginModelHandle},
768 stubs::*,
769 },
770 enums::{AccountType, OrderSide, OrderType},
771 events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
772 identifiers::{
773 AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
774 stubs::{uuid4, *},
775 },
776 instruments::{
777 CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
778 stubs::{binary_option, option_contract_appl, *},
779 },
780 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
781 position::Position,
782 types::{
783 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
784 money::{MONEY_RAW_MAX, MONEY_RAW_MIN},
785 },
786 };
787
788 struct CustomMarginModel;
789
790 impl MarginModel for CustomMarginModel {
791 fn name(&self) -> &'static str {
792 "custom"
793 }
794
795 fn calculate_initial_margin(
796 &self,
797 _instrument: &dyn Instrument,
798 _quantity: Quantity,
799 _price: Price,
800 _leverage: Decimal,
801 _use_quote_for_inverse: Option<bool>,
802 ) -> anyhow::Result<Money> {
803 Ok(Money::from("12.34 USD"))
804 }
805
806 fn calculate_maintenance_margin(
807 &self,
808 _instrument: &dyn Instrument,
809 _quantity: Quantity,
810 _price: Price,
811 _leverage: Decimal,
812 _use_quote_for_inverse: Option<bool>,
813 ) -> anyhow::Result<Money> {
814 Ok(Money::from("5.67 USD"))
815 }
816 }
817
818 #[rstest]
819 fn test_display(margin_account: MarginAccount) {
820 assert_eq!(
821 margin_account.to_string(),
822 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
823 );
824 }
825
826 #[rstest]
827 fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
828 assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
829 assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
830 }
831
832 #[rstest]
833 fn test_base_account_properties(
834 margin_account: MarginAccount,
835 margin_account_state: AccountState,
836 ) {
837 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
838 assert_eq!(
839 margin_account.last_event(),
840 Some(margin_account_state.clone())
841 );
842 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
843 assert_eq!(margin_account.event_count(), 1);
844 assert_eq!(
845 margin_account.balance_total(None),
846 Some(Money::from("1525000 USD"))
847 );
848 assert_eq!(
849 margin_account.balance_free(None),
850 Some(Money::from("1500000 USD"))
851 );
852 assert_eq!(
853 margin_account.balance_locked(None),
854 Some(Money::from("25000 USD"))
855 );
856 let mut balances_total_expected = IndexMap::new();
857 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
858 assert_eq!(margin_account.balances_total(), balances_total_expected);
859 let mut balances_free_expected = IndexMap::new();
860 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
861 assert_eq!(margin_account.balances_free(), balances_free_expected);
862 let mut balances_locked_expected = IndexMap::new();
863 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
864 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
865 let margin_balance = margin_account_state.margins[0];
866 let instrument_id = margin_balance
867 .instrument_id
868 .expect("stub margin balance carries a concrete instrument_id");
869 let mut initial_margins_expected = IndexMap::new();
870 initial_margins_expected.insert(instrument_id, margin_balance.initial);
871 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
872 let mut maintenance_margins_expected = IndexMap::new();
873 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
874 assert_eq!(
875 margin_account.maintenance_margins(),
876 maintenance_margins_expected
877 );
878 }
879
880 #[rstest]
881 fn test_set_default_leverage(mut margin_account: MarginAccount) {
882 assert_eq!(margin_account.default_leverage, Decimal::ONE);
883 margin_account.set_default_leverage(Decimal::from(10));
884 assert_eq!(margin_account.default_leverage, Decimal::from(10));
885 }
886
887 #[rstest]
888 fn test_get_leverage_default_leverage(
889 margin_account: MarginAccount,
890 instrument_id_aud_usd_sim: InstrumentId,
891 ) {
892 assert_eq!(
893 margin_account.get_leverage(&instrument_id_aud_usd_sim),
894 Decimal::ONE
895 );
896 }
897
898 #[rstest]
899 fn test_set_leverage(
900 mut margin_account: MarginAccount,
901 instrument_id_aud_usd_sim: InstrumentId,
902 ) {
903 assert_eq!(margin_account.leverages.len(), 0);
904 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
905 assert_eq!(margin_account.leverages.len(), 1);
906 assert_eq!(
907 margin_account.get_leverage(&instrument_id_aud_usd_sim),
908 Decimal::from(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 }
998
999 #[rstest]
1000 fn test_clear_initial_margin_preserves_maintenance(
1001 mut margin_account: MarginAccount,
1002 instrument_id_aud_usd_sim: InstrumentId,
1003 ) {
1004 margin_account.update_margin(MarginBalance::new(
1005 Money::from("1000 USD"),
1006 Money::from("500 USD"),
1007 Some(instrument_id_aud_usd_sim),
1008 ));
1009
1010 margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
1011
1012 let margin = margin_account
1013 .margin(&instrument_id_aud_usd_sim)
1014 .expect("margin should retain non-zero maintenance");
1015 assert_eq!(margin.initial, Money::from("0 USD"));
1016 assert_eq!(margin.maintenance, Money::from("500 USD"));
1017 }
1018
1019 #[rstest]
1020 fn test_clear_maintenance_margin_removes_empty_entry(
1021 mut margin_account: MarginAccount,
1022 instrument_id_aud_usd_sim: InstrumentId,
1023 ) {
1024 margin_account.update_margin(MarginBalance::new(
1025 Money::from("0 USD"),
1026 Money::from("500 USD"),
1027 Some(instrument_id_aud_usd_sim),
1028 ));
1029
1030 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1031
1032 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1033 }
1034
1035 #[rstest]
1036 fn test_clear_maintenance_margin_preserves_initial(
1037 mut margin_account: MarginAccount,
1038 instrument_id_aud_usd_sim: InstrumentId,
1039 ) {
1040 margin_account.update_margin(MarginBalance::new(
1041 Money::from("1000 USD"),
1042 Money::from("500 USD"),
1043 Some(instrument_id_aud_usd_sim),
1044 ));
1045
1046 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1047
1048 let margin = margin_account
1049 .margin(&instrument_id_aud_usd_sim)
1050 .expect("margin should retain non-zero initial");
1051 assert_eq!(margin.initial, Money::from("1000 USD"));
1052 assert_eq!(margin.maintenance, Money::from("0 USD"));
1053 }
1054
1055 #[rstest]
1056 fn test_apply_replaces_margin_balances_from_event(
1057 mut margin_account: MarginAccount,
1058 margin_account_state: AccountState,
1059 ) {
1060 let old_instrument_id = margin_account_state.margins[0]
1061 .instrument_id
1062 .expect("stub margin balance carries a concrete instrument_id");
1063 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1064 let event = AccountState::new(
1065 margin_account_state.account_id,
1066 AccountType::Margin,
1067 margin_account_state.balances.clone(),
1068 vec![MarginBalance::new(
1069 Money::from("12500 USD"),
1070 Money::from("25000 USD"),
1071 Some(new_instrument_id),
1072 )],
1073 true,
1074 uuid4(),
1075 1.into(),
1076 1.into(),
1077 margin_account_state.base_currency,
1078 );
1079
1080 margin_account.apply(event).unwrap();
1081
1082 assert_eq!(
1083 margin_account.initial_margin(new_instrument_id),
1084 Money::from("12500 USD")
1085 );
1086 assert_eq!(
1087 margin_account.maintenance_margin(new_instrument_id),
1088 Money::from("25000 USD")
1089 );
1090 assert!(margin_account.margin(&old_instrument_id).is_none());
1091 }
1092
1093 #[rstest]
1094 fn test_apply_routes_account_margins_by_currency(
1095 mut margin_account: MarginAccount,
1096 margin_account_state: AccountState,
1097 ) {
1098 let usd = Currency::USD();
1099 let event = AccountState::new(
1100 margin_account_state.account_id,
1101 AccountType::Margin,
1102 margin_account_state.balances.clone(),
1103 vec![MarginBalance::new(
1104 Money::from("12500 USD"),
1105 Money::from("25000 USD"),
1106 None,
1107 )],
1108 true,
1109 uuid4(),
1110 1.into(),
1111 1.into(),
1112 margin_account_state.base_currency,
1113 );
1114
1115 margin_account.apply(event).unwrap();
1116
1117 assert!(margin_account.margins.is_empty());
1118 assert_eq!(margin_account.account_margins.len(), 1);
1119 assert_eq!(
1120 margin_account.account_initial_margin(&usd),
1121 Some(Money::from("12500 USD"))
1122 );
1123 assert_eq!(
1124 margin_account.account_maintenance_margin(&usd),
1125 Some(Money::from("25000 USD"))
1126 );
1127 assert_eq!(
1128 margin_account.total_initial_margin(usd),
1129 Money::from("12500 USD")
1130 );
1131 }
1132
1133 #[rstest]
1134 fn test_apply_empty_event_preserves_margin_balances(
1135 mut margin_account: MarginAccount,
1136 margin_account_state: AccountState,
1137 ) {
1138 let instrument_id = margin_account_state.margins[0]
1139 .instrument_id
1140 .expect("stub margin balance carries a concrete instrument_id");
1141 let initial_margin = margin_account.initial_margin(instrument_id);
1142 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1143
1144 let empty_event = AccountState::new(
1145 margin_account_state.account_id,
1146 AccountType::Margin,
1147 vec![],
1148 vec![],
1149 true,
1150 uuid4(),
1151 1.into(),
1152 1.into(),
1153 margin_account_state.base_currency,
1154 );
1155
1156 margin_account.apply(empty_event).unwrap();
1157
1158 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1159 assert_eq!(
1160 margin_account.maintenance_margin(instrument_id),
1161 maintenance_margin
1162 );
1163 assert_eq!(margin_account.event_count(), 2);
1164 }
1165
1166 #[rstest]
1167 fn test_calculate_margin_init_with_leverage(
1168 mut margin_account: MarginAccount,
1169 audusd_sim: CurrencyPair,
1170 ) {
1171 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1172 let result = margin_account
1173 .calculate_initial_margin(
1174 &audusd_sim,
1175 Quantity::from(100_000),
1176 Price::from("0.8000"),
1177 None,
1178 )
1179 .unwrap();
1180 assert_eq!(result, Money::from("48.00 USD"));
1181 }
1182
1183 #[rstest]
1184 fn test_custom_margin_model_through_account(
1185 mut margin_account: MarginAccount,
1186 audusd_sim: CurrencyPair,
1187 ) {
1188 margin_account.set_margin_model(MarginModelHandle::new(CustomMarginModel));
1189
1190 let initial = margin_account
1191 .calculate_initial_margin(
1192 &audusd_sim,
1193 Quantity::from(100_000),
1194 Price::from("0.8000"),
1195 None,
1196 )
1197 .unwrap();
1198 let maintenance = margin_account
1199 .calculate_maintenance_margin(
1200 &audusd_sim,
1201 Quantity::from(100_000),
1202 Price::from("0.8000"),
1203 None,
1204 )
1205 .unwrap();
1206
1207 assert_eq!(margin_account.margin_model().name(), "custom");
1208 assert_eq!(initial, Money::from("12.34 USD"));
1209 assert_eq!(maintenance, Money::from("5.67 USD"));
1210 }
1211
1212 #[rstest]
1213 fn test_calculate_margin_init_with_default_leverage(
1214 mut margin_account: MarginAccount,
1215 audusd_sim: CurrencyPair,
1216 ) {
1217 margin_account.set_default_leverage(Decimal::from(10));
1218 let result = margin_account
1219 .calculate_initial_margin(
1220 &audusd_sim,
1221 Quantity::from(100_000),
1222 Price::from("0.8"),
1223 None,
1224 )
1225 .unwrap();
1226 assert_eq!(result, Money::from("240.00 USD"));
1227 }
1228
1229 #[rstest]
1230 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1231 mut margin_account: MarginAccount,
1232 xbtusd_bitmex: CryptoPerpetual,
1233 ) {
1234 let result_use_quote_inverse_true = margin_account
1235 .calculate_initial_margin(
1236 &xbtusd_bitmex,
1237 Quantity::from(100_000),
1238 Price::from("11493.60"),
1239 Some(false),
1240 )
1241 .unwrap();
1242 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1243 let result_use_quote_inverse_false = margin_account
1244 .calculate_initial_margin(
1245 &xbtusd_bitmex,
1246 Quantity::from(100_000),
1247 Price::from("11493.60"),
1248 Some(true),
1249 )
1250 .unwrap();
1251 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1252 }
1253
1254 #[rstest]
1255 fn test_calculate_margin_maintenance_with_no_leverage(
1256 mut margin_account: MarginAccount,
1257 xbtusd_bitmex: CryptoPerpetual,
1258 ) {
1259 let result = margin_account
1260 .calculate_maintenance_margin(
1261 &xbtusd_bitmex,
1262 Quantity::from(100_000),
1263 Price::from("11493.60"),
1264 None,
1265 )
1266 .unwrap();
1267 assert_eq!(result, Money::from("0.03045173 BTC"));
1268 }
1269
1270 #[rstest]
1271 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1272 mut margin_account: MarginAccount,
1273 audusd_sim: CurrencyPair,
1274 ) {
1275 margin_account.set_default_leverage(Decimal::from(50));
1276 let result = margin_account
1277 .calculate_maintenance_margin(
1278 &audusd_sim,
1279 Quantity::from(1_000_000),
1280 Price::from("1"),
1281 None,
1282 )
1283 .unwrap();
1284 assert_eq!(result, Money::from("600.00 USD"));
1285 }
1286
1287 #[rstest]
1288 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1289 mut margin_account: MarginAccount,
1290 xbtusd_bitmex: CryptoPerpetual,
1291 ) {
1292 margin_account.set_default_leverage(Decimal::from(10));
1293 let result = margin_account
1294 .calculate_maintenance_margin(
1295 &xbtusd_bitmex,
1296 Quantity::from(100_000),
1297 Price::from("100000.00"),
1298 None,
1299 )
1300 .unwrap();
1301 assert_eq!(result, Money::from("0.00035000 BTC"));
1302 }
1303
1304 #[rstest]
1305 fn test_calculate_pnls_github_issue_2657() {
1306 let account_state = margin_account_state();
1308 let account = MarginAccount::new(account_state, false);
1309
1310 let btcusdt = currency_pair_btcusdt();
1312 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1313
1314 let fill1 = OrderFilledSpec::builder()
1316 .instrument_id(btcusdt_any.id())
1317 .client_order_id(ClientOrderId::from("O-1"))
1318 .venue_order_id(VenueOrderId::from("V-1"))
1319 .trade_id(TradeId::from("T-1"))
1320 .last_qty(Quantity::from("0.001"))
1321 .last_px(Price::from("50000.00"))
1322 .currency(btcusdt_any.quote_currency())
1323 .ts_event(UnixNanos::from(1_000_000_000))
1324 .position_id(PositionId::from("P-GITHUB-2657"))
1325 .build();
1326
1327 let position = Position::new(&btcusdt_any, fill1);
1328
1329 let fill2 = OrderFilledSpec::builder()
1331 .instrument_id(btcusdt_any.id())
1332 .client_order_id(ClientOrderId::from("O-2"))
1333 .venue_order_id(VenueOrderId::from("V-2"))
1334 .trade_id(TradeId::from("T-2"))
1335 .order_side(OrderSide::Sell)
1336 .last_qty(Quantity::from("0.002")) .last_px(Price::from("50075.00"))
1338 .currency(btcusdt_any.quote_currency())
1339 .ts_event(UnixNanos::from(2_000_000_000))
1340 .position_id(PositionId::from("P-GITHUB-2657"))
1341 .build();
1342
1343 let pnls = account
1345 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1346 .unwrap();
1347
1348 assert_eq!(pnls.len(), 1);
1350
1351 let expected_pnl = Money::from("0.075 USDT");
1354 assert_eq!(pnls[0], expected_pnl);
1355 }
1356
1357 #[rstest]
1358 #[should_panic(expected = "not positive")]
1359 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1360 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1361 }
1362
1363 #[rstest]
1364 #[should_panic(expected = "not positive")]
1365 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1366 margin_account.set_default_leverage(Decimal::ZERO);
1367 }
1368
1369 #[rstest]
1370 #[should_panic(expected = "not positive")]
1371 fn test_set_leverage_negative_panics(
1372 mut margin_account: MarginAccount,
1373 audusd_sim: CurrencyPair,
1374 ) {
1375 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1376 }
1377
1378 #[rstest]
1379 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1380 use nautilus_core::UnixNanos;
1381
1382 use crate::{
1383 events::order::spec::OrderFilledSpec,
1384 identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1385 instruments::InstrumentAny,
1386 position::Position,
1387 types::{Price, Quantity},
1388 };
1389
1390 let account_state = margin_account_state();
1392 let account = MarginAccount::new(account_state, false);
1393
1394 let btcusdt = currency_pair_btcusdt();
1396 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1397
1398 let fill1 = OrderFilledSpec::builder()
1400 .instrument_id(btcusdt.id)
1401 .client_order_id(ClientOrderId::from("O-1"))
1402 .venue_order_id(VenueOrderId::from("V-1"))
1403 .trade_id(TradeId::from("T-1"))
1404 .last_qty(Quantity::from("1.0"))
1405 .last_px(Price::from("50000.00"))
1406 .currency(btcusdt.quote_currency)
1407 .ts_event(UnixNanos::from(1_000_000_000))
1408 .position_id(PositionId::from("P-123456"))
1409 .build();
1410
1411 let position = Position::new(&btcusdt_any, fill1);
1412
1413 let fill2 = OrderFilledSpec::builder()
1415 .instrument_id(btcusdt.id)
1416 .client_order_id(ClientOrderId::from("O-2"))
1417 .venue_order_id(VenueOrderId::from("V-2"))
1418 .trade_id(TradeId::from("T-2"))
1419 .last_qty(Quantity::from("0.5"))
1420 .last_px(Price::from("51000.00"))
1421 .currency(btcusdt.quote_currency)
1422 .ts_event(UnixNanos::from(2_000_000_000))
1423 .position_id(PositionId::from("P-123456"))
1424 .build();
1425
1426 let pnls = account
1428 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1429 .unwrap();
1430
1431 assert_eq!(pnls.len(), 0);
1433 }
1434
1435 #[rstest]
1436 fn test_margin_accessor(
1437 mut margin_account: MarginAccount,
1438 instrument_id_aud_usd_sim: InstrumentId,
1439 ) {
1440 let margin_balance = MarginBalance::new(
1441 Money::from("1000 USD"),
1442 Money::from("500 USD"),
1443 Some(instrument_id_aud_usd_sim),
1444 );
1445
1446 margin_account.update_margin(margin_balance);
1447
1448 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1449 assert!(retrieved.is_some());
1450 let retrieved = retrieved.unwrap();
1451 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1452 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1453 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1454 }
1455
1456 #[rstest]
1457 fn test_clear_margin(
1458 mut margin_account: MarginAccount,
1459 instrument_id_aud_usd_sim: InstrumentId,
1460 ) {
1461 let margin_balance = MarginBalance::new(
1462 Money::from("1000 USD"),
1463 Money::from("500 USD"),
1464 Some(instrument_id_aud_usd_sim),
1465 );
1466
1467 margin_account.update_margin(margin_balance);
1468 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1469
1470 margin_account.clear_margin(instrument_id_aud_usd_sim);
1471 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1472 }
1473
1474 #[rstest]
1475 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1476 let usd = Currency::USD();
1477 let margin_balance =
1478 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1479
1480 margin_account.update_margin(margin_balance);
1481
1482 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1483 assert_eq!(
1484 margin_account.account_initial_margin(&usd),
1485 Some(Money::from("200 USD"))
1486 );
1487 assert_eq!(
1488 margin_account.account_maintenance_margin(&usd),
1489 Some(Money::from("100 USD"))
1490 );
1491
1492 margin_account.clear_account_margin(usd);
1493 assert!(margin_account.account_margin(&usd).is_none());
1494 }
1495
1496 fn multi_currency_margin_account(reported: Money) -> MarginAccount {
1499 let state = AccountState::new(
1500 AccountId::from("BYBIT-001"),
1501 AccountType::Margin,
1502 vec![AccountBalance::new(
1503 reported,
1504 Money::zero(reported.currency),
1505 reported,
1506 )],
1507 Vec::new(),
1508 true,
1509 uuid4(),
1510 0.into(),
1511 0.into(),
1512 None,
1513 );
1514 MarginAccount::new(state, true)
1515 }
1516
1517 #[rstest]
1518 fn test_margin_update_leaves_unreported_currency_absent() {
1519 let usdt = Currency::USDT();
1520 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1521 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1522
1523 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1524
1525 assert_eq!(account.balance_total(Some(usdt)), None);
1526 assert_eq!(account.balance_locked(Some(usdt)), None);
1527 assert_eq!(account.balance_free(Some(usdt)), None);
1528 assert_eq!(
1529 account.balances.keys().copied().collect::<Vec<_>>(),
1530 vec![Currency::USD()]
1531 );
1532 assert_eq!(
1533 account.initial_margin(instrument_id),
1534 Money::from("5000 USDT")
1535 );
1536 assert_eq!(
1537 account.balance_free(Some(Currency::USD())),
1538 Some(Money::from("1000000 USD"))
1539 );
1540 }
1541
1542 #[rstest]
1543 fn test_margin_update_locks_reported_currency() {
1544 let usdt = Currency::USDT();
1545 let mut account = multi_currency_margin_account(Money::from("100000 USDT"));
1546 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1547
1548 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1549
1550 assert_eq!(
1551 account.balance_total(Some(usdt)),
1552 Some(Money::from("100000 USDT"))
1553 );
1554 assert_eq!(
1555 account.balance_locked(Some(usdt)),
1556 Some(Money::from("5000 USDT"))
1557 );
1558 assert_eq!(
1559 account.balance_free(Some(usdt)),
1560 Some(Money::from("95000 USDT"))
1561 );
1562 }
1563
1564 #[rstest]
1565 fn test_margin_update_locks_currency_reported_after_the_margin() {
1566 let usdt = Currency::USDT();
1567 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1568 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1569
1570 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1571 account
1572 .apply(AccountState::new(
1573 AccountId::from("BYBIT-001"),
1574 AccountType::Margin,
1575 vec![AccountBalance::new(
1576 Money::from("100000 USDT"),
1577 Money::zero(usdt),
1578 Money::from("100000 USDT"),
1579 )],
1580 Vec::new(),
1581 true,
1582 uuid4(),
1583 0.into(),
1584 0.into(),
1585 None,
1586 ))
1587 .unwrap();
1588 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1589
1590 assert_eq!(
1591 account.balance_locked(Some(usdt)),
1592 Some(Money::from("5000 USDT"))
1593 );
1594 assert_eq!(
1595 account.balance_free(Some(usdt)),
1596 Some(Money::from("95000 USDT"))
1597 );
1598 }
1599
1600 #[rstest]
1601 fn test_recalculate_balance_clamps_when_margin_exceeds_total() {
1602 let usdt = Currency::USDT();
1603 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1604
1605 account.update_initial_margin(
1606 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
1607 Money::from("1500 USDT"),
1608 );
1609
1610 assert_eq!(
1611 account.balance_total(Some(usdt)),
1612 Some(Money::from("1000 USDT"))
1613 );
1614 assert_eq!(
1615 account.balance_locked(Some(usdt)),
1616 Some(Money::from("1000 USDT"))
1617 );
1618 assert_eq!(
1619 account.balance_free(Some(usdt)),
1620 Some(Money::from("0 USDT"))
1621 );
1622 }
1623
1624 #[rstest]
1625 fn test_recalculate_balance_keeps_locked_non_negative_when_total_negative() {
1626 let usd = Currency::USD();
1627 let mut account = multi_currency_margin_account(Money::from("-1000 USD"));
1628
1629 account.update_initial_margin(InstrumentId::from("EURUSD.SIM"), Money::from("500 USD"));
1630
1631 assert_eq!(
1632 account.balance_total(Some(usd)),
1633 Some(Money::from("-1000 USD"))
1634 );
1635 assert_eq!(
1636 account.balance_locked(Some(usd)),
1637 Some(Money::from("0 USD"))
1638 );
1639 assert_eq!(
1640 account.balance_free(Some(usd)),
1641 Some(Money::from("-1000 USD"))
1642 );
1643 }
1644
1645 #[rstest]
1646 fn test_total_margin_sums_per_instrument_and_account_wide(
1647 mut margin_account: MarginAccount,
1648 instrument_id_aud_usd_sim: InstrumentId,
1649 ) {
1650 let usd = Currency::USD();
1651 let baseline_initial = margin_account.total_initial_margin(usd);
1652 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1653
1654 margin_account.update_margin(MarginBalance::new(
1655 Money::from("100 USD"),
1656 Money::from("50 USD"),
1657 Some(instrument_id_aud_usd_sim),
1658 ));
1659 margin_account.update_margin(MarginBalance::new(
1660 Money::from("200 USD"),
1661 Money::from("150 USD"),
1662 None,
1663 ));
1664
1665 assert_eq!(
1666 margin_account.total_initial_margin(usd).raw,
1667 baseline_initial.raw + Money::from("300 USD").raw,
1668 );
1669 assert_eq!(
1670 margin_account.total_maintenance_margin(usd).raw,
1671 baseline_maintenance.raw + Money::from("200 USD").raw,
1672 );
1673 }
1674
1675 #[rstest]
1676 fn test_total_margin_clamps_domain_overflow(
1677 mut margin_account: MarginAccount,
1678 instrument_id_aud_usd_sim: InstrumentId,
1679 ) {
1680 let usd = Currency::USD();
1681 let max = Money::from_raw(MONEY_RAW_MAX, usd);
1682 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1683
1684 margin_account.margins.insert(
1685 instrument_id_aud_usd_sim,
1686 MarginBalance::new(max, max, Some(instrument_id_aud_usd_sim)),
1687 );
1688 margin_account.margins.insert(
1689 other_instrument,
1690 MarginBalance::new(max, max, Some(other_instrument)),
1691 );
1692
1693 assert_eq!(margin_account.total_initial_margin(usd), max);
1694 assert_eq!(margin_account.total_maintenance_margin(usd), max);
1695 }
1696
1697 #[rstest]
1698 fn test_total_margin_clamps_negative_domain_overflow(
1699 mut margin_account: MarginAccount,
1700 instrument_id_aud_usd_sim: InstrumentId,
1701 ) {
1702 let usd = Currency::USD();
1703 let min = Money::from_raw(MONEY_RAW_MIN, usd);
1704 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1705
1706 margin_account.margins.insert(
1707 instrument_id_aud_usd_sim,
1708 MarginBalance::new(min, min, Some(instrument_id_aud_usd_sim)),
1709 );
1710 margin_account.margins.insert(
1711 other_instrument,
1712 MarginBalance::new(min, min, Some(other_instrument)),
1713 );
1714
1715 assert_eq!(margin_account.total_initial_margin(usd), min);
1716 assert_eq!(margin_account.total_maintenance_margin(usd), min);
1717 }
1718
1719 #[rstest]
1720 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1721 let option = option_contract_appl();
1722 let option_any = InstrumentAny::OptionContract(option.clone());
1723
1724 let order = OrderTestBuilder::new(OrderType::Market)
1725 .instrument_id(option.id)
1726 .side(OrderSide::Buy)
1727 .quantity(Quantity::from("10"))
1728 .build();
1729
1730 let fill = TestOrderEventStubs::filled(
1731 &order,
1732 &option_any,
1733 None,
1734 Some(PositionId::new("P-OPT-001")),
1735 Some(Price::from("5.50")),
1736 None,
1737 None,
1738 None,
1739 None,
1740 Some(AccountId::from("SIM-001")),
1741 );
1742
1743 let fill_owned: crate::events::OrderFilled = fill.into();
1744 let pnls = margin_account
1745 .calculate_pnls(&option_any, &fill_owned, None)
1746 .unwrap();
1747
1748 assert_eq!(pnls.len(), 1);
1751 assert_eq!(pnls[0], Money::from("-55 USD"));
1752 }
1753
1754 #[rstest]
1755 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1756 let option = option_contract_appl();
1757 let option_any = InstrumentAny::OptionContract(option.clone());
1758
1759 let order = OrderTestBuilder::new(OrderType::Market)
1760 .instrument_id(option.id)
1761 .side(OrderSide::Sell)
1762 .quantity(Quantity::from("10"))
1763 .build();
1764
1765 let fill = TestOrderEventStubs::filled(
1766 &order,
1767 &option_any,
1768 None,
1769 Some(PositionId::new("P-OPT-002")),
1770 Some(Price::from("5.50")),
1771 None,
1772 None,
1773 None,
1774 None,
1775 Some(AccountId::from("SIM-001")),
1776 );
1777
1778 let fill_owned: crate::events::OrderFilled = fill.into();
1779 let pnls = margin_account
1780 .calculate_pnls(&option_any, &fill_owned, None)
1781 .unwrap();
1782
1783 assert_eq!(pnls.len(), 1);
1786 assert_eq!(pnls[0], Money::from("55 USD"));
1787 }
1788
1789 #[rstest]
1790 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1791 let binary = binary_option();
1792 let binary_any = InstrumentAny::BinaryOption(binary);
1793
1794 let order = OrderTestBuilder::new(OrderType::Market)
1795 .instrument_id(binary_any.id())
1796 .side(OrderSide::Buy)
1797 .quantity(Quantity::from("100"))
1798 .build();
1799
1800 let fill = TestOrderEventStubs::filled(
1801 &order,
1802 &binary_any,
1803 None,
1804 Some(PositionId::new("P-BIN-001")),
1805 Some(Price::from("0.65")),
1806 None,
1807 None,
1808 None,
1809 None,
1810 Some(AccountId::from("SIM-001")),
1811 );
1812
1813 let fill_owned: crate::events::OrderFilled = fill.into();
1814 let pnls = margin_account
1815 .calculate_pnls(&binary_any, &fill_owned, None)
1816 .unwrap();
1817
1818 assert_eq!(pnls.len(), 1);
1819 assert!(pnls[0].as_f64() < 0.0);
1820 }
1821}