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 margin_model::{MarginModel, MarginModelHandle},
49 },
50 enums::{AccountType, OrderSide},
51 events::{AccountState, OrderFilled},
52 identifiers::InstrumentId,
53 instruments::{Instrument, InstrumentAny},
54 position::Position,
55 types::{
56 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
57 fixed::check_float_precision,
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 let mut pnls: Vec<Money> = Vec::new();
635
636 if instrument.instrument_class().is_premium_based() {
638 let notional =
639 instrument.try_calculate_notional_value(fill.last_qty, fill.last_px, None)?;
640 let pnl = if fill.order_side == OrderSide::Buy {
641 -notional
642 } else {
643 notional
644 };
645 pnls.push(pnl);
646 return Ok(pnls);
647 }
648
649 if let Some(ref pos) = position
651 && pos.quantity.is_positive()
652 && pos.entry != fill.order_side
653 {
654 let mut pnl_quantity = fill.last_qty.min(pos.quantity);
657 pnl_quantity.precision = fill.last_qty.precision;
658 check_float_precision(fill.last_px.precision)?;
659 let pnl =
660 pos.try_calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity)?;
661 pnls.push(pnl);
662 }
663
664 Ok(pnls)
665 }
666}
667
668impl PartialEq for MarginAccount {
669 fn eq(&self, other: &Self) -> bool {
670 self.id == other.id
671 }
672}
673
674impl Eq for MarginAccount {}
675
676impl Display for MarginAccount {
677 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 write!(
679 f,
680 "MarginAccount(id={}, type={}, base={})",
681 self.id,
682 self.account_type,
683 self.base_currency.map_or_else(
684 || "None".to_string(),
685 |base_currency| format!("{}", base_currency.code)
686 ),
687 )
688 }
689}
690
691impl Hash for MarginAccount {
692 fn hash<H: Hasher>(&self, state: &mut H) {
693 self.id.hash(state);
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use ahash::AHashMap;
700 use indexmap::IndexMap;
701 use nautilus_core::UnixNanos;
702 use rstest::rstest;
703 use rust_decimal::Decimal;
704 use rust_decimal_macros::dec;
705
706 use crate::{
707 accounts::{
708 Account, MarginAccount,
709 margin_model::{MarginModel, MarginModelHandle},
710 stubs::*,
711 },
712 enums::{AccountType, LiquiditySide, OrderSide, OrderType},
713 events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
714 fees::{MakerTakerFeeRates, MakerTakerFeeSchedule},
715 identifiers::{
716 AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
717 stubs::{uuid4, *},
718 },
719 instruments::{
720 CryptoOption, CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
721 stubs::{binary_option, option_contract_appl, *},
722 },
723 orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
724 position::Position,
725 types::{
726 AccountBalance, Currency, MarginBalance, Money, Price, Quantity,
727 money::{MONEY_RAW_MAX, MONEY_RAW_MIN},
728 },
729 };
730
731 struct CustomMarginModel;
732
733 impl MarginModel for CustomMarginModel {
734 fn name(&self) -> &'static str {
735 "custom"
736 }
737
738 fn calculate_initial_margin(
739 &self,
740 _instrument: &dyn Instrument,
741 _quantity: Quantity,
742 _price: Price,
743 _leverage: Decimal,
744 _use_quote_for_inverse: Option<bool>,
745 ) -> anyhow::Result<Money> {
746 Ok(Money::from("12.34 USD"))
747 }
748
749 fn calculate_maintenance_margin(
750 &self,
751 _instrument: &dyn Instrument,
752 _quantity: Quantity,
753 _price: Price,
754 _leverage: Decimal,
755 _use_quote_for_inverse: Option<bool>,
756 ) -> anyhow::Result<Money> {
757 Ok(Money::from("5.67 USD"))
758 }
759 }
760
761 #[rstest]
762 fn test_display(margin_account: MarginAccount) {
763 assert_eq!(
764 margin_account.to_string(),
765 "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
766 );
767 }
768
769 #[rstest]
770 fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
771 assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
772 assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
773 }
774
775 #[rstest]
776 fn test_base_account_properties(
777 margin_account: MarginAccount,
778 margin_account_state: AccountState,
779 ) {
780 assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
781 assert_eq!(
782 margin_account.last_event(),
783 Some(margin_account_state.clone())
784 );
785 assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
786 assert_eq!(margin_account.event_count(), 1);
787 assert_eq!(
788 margin_account.balance_total(None),
789 Some(Money::from("1525000 USD"))
790 );
791 assert_eq!(
792 margin_account.balance_free(None),
793 Some(Money::from("1500000 USD"))
794 );
795 assert_eq!(
796 margin_account.balance_locked(None),
797 Some(Money::from("25000 USD"))
798 );
799 let mut balances_total_expected = IndexMap::new();
800 balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
801 assert_eq!(margin_account.balances_total(), balances_total_expected);
802 let mut balances_free_expected = IndexMap::new();
803 balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
804 assert_eq!(margin_account.balances_free(), balances_free_expected);
805 let mut balances_locked_expected = IndexMap::new();
806 balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
807 assert_eq!(margin_account.balances_locked(), balances_locked_expected);
808 let margin_balance = margin_account_state.margins[0];
809 let instrument_id = margin_balance
810 .instrument_id
811 .expect("stub margin balance carries a concrete instrument_id");
812 let mut initial_margins_expected = IndexMap::new();
813 initial_margins_expected.insert(instrument_id, margin_balance.initial);
814 assert_eq!(margin_account.initial_margins(), initial_margins_expected);
815 let mut maintenance_margins_expected = IndexMap::new();
816 maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
817 assert_eq!(
818 margin_account.maintenance_margins(),
819 maintenance_margins_expected
820 );
821 }
822
823 #[rstest]
824 fn test_set_default_leverage(mut margin_account: MarginAccount) {
825 assert_eq!(margin_account.default_leverage, Decimal::ONE);
826 margin_account.set_default_leverage(Decimal::from(10));
827 assert_eq!(margin_account.default_leverage, Decimal::from(10));
828 }
829
830 #[rstest]
831 fn test_get_leverage_default_leverage(
832 margin_account: MarginAccount,
833 instrument_id_aud_usd_sim: InstrumentId,
834 ) {
835 assert_eq!(
836 margin_account.get_leverage(&instrument_id_aud_usd_sim),
837 Decimal::ONE
838 );
839 }
840
841 #[rstest]
842 fn test_set_leverage(
843 mut margin_account: MarginAccount,
844 instrument_id_aud_usd_sim: InstrumentId,
845 ) {
846 assert_eq!(margin_account.leverages.len(), 0);
847 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
848 assert_eq!(margin_account.leverages.len(), 1);
849 assert_eq!(
850 margin_account.get_leverage(&instrument_id_aud_usd_sim),
851 Decimal::from(10)
852 );
853 }
854
855 #[rstest]
856 #[case(dec!(0))]
857 #[case(dec!(-2.5))]
858 fn test_try_leverage_setters_reject_non_positive_without_mutation(
859 mut margin_account: MarginAccount,
860 instrument_id_aud_usd_sim: InstrumentId,
861 #[case] leverage: Decimal,
862 ) {
863 margin_account
864 .try_set_leverage(instrument_id_aud_usd_sim, dec!(10))
865 .unwrap();
866 margin_account.try_set_default_leverage(dec!(5)).unwrap();
867
868 let default_error = margin_account
869 .try_set_default_leverage(leverage)
870 .unwrap_err();
871 let instrument_error = margin_account
872 .try_set_leverage(instrument_id_aud_usd_sim, leverage)
873 .unwrap_err();
874
875 let expected = format!("invalid Decimal for 'leverage' not positive, was {leverage}");
876 assert_eq!(default_error.to_string(), expected);
877 assert_eq!(instrument_error.to_string(), expected);
878 assert_eq!(margin_account.default_leverage, dec!(5));
879 assert_eq!(
880 margin_account.leverages,
881 AHashMap::from([(instrument_id_aud_usd_sim, dec!(10))])
882 );
883 }
884
885 #[rstest]
886 fn test_is_unleveraged_with_leverage_returns_false(
887 mut margin_account: MarginAccount,
888 instrument_id_aud_usd_sim: InstrumentId,
889 ) {
890 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
891 assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
892 }
893
894 #[rstest]
895 fn test_is_unleveraged_with_no_leverage_returns_true(
896 mut margin_account: MarginAccount,
897 instrument_id_aud_usd_sim: InstrumentId,
898 ) {
899 margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
900 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
901 }
902
903 #[rstest]
904 fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
905 margin_account: MarginAccount,
906 instrument_id_aud_usd_sim: InstrumentId,
907 ) {
908 assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
909 }
910
911 #[rstest]
912 fn test_update_margin_init(
913 mut margin_account: MarginAccount,
914 instrument_id_aud_usd_sim: InstrumentId,
915 ) {
916 assert_eq!(margin_account.margins.len(), 1);
917 let margin = Money::from("10000 USD");
918 margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
919 assert_eq!(
920 margin_account.initial_margin(instrument_id_aud_usd_sim),
921 margin
922 );
923 assert_eq!(margin_account.margins.len(), 2);
924 assert_eq!(
925 margin_account
926 .margins
927 .get(&instrument_id_aud_usd_sim)
928 .expect("AUD/USD margin should exist")
929 .initial,
930 margin
931 );
932 assert_eq!(
933 margin_account
934 .margins
935 .get(&instrument_id_aud_usd_sim)
936 .expect("AUD/USD margin should exist")
937 .maintenance,
938 Money::zero(margin.currency)
939 );
940 }
941
942 #[rstest]
943 fn test_update_margin_maintenance(
944 mut margin_account: MarginAccount,
945 instrument_id_aud_usd_sim: InstrumentId,
946 ) {
947 let margin = Money::from("10000 USD");
948 margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
949 assert_eq!(
950 margin_account.maintenance_margin(instrument_id_aud_usd_sim),
951 margin
952 );
953 assert_eq!(margin_account.margins.len(), 2);
954 assert_eq!(
955 margin_account
956 .margins
957 .get(&instrument_id_aud_usd_sim)
958 .expect("AUD/USD margin should exist")
959 .maintenance,
960 margin
961 );
962 assert_eq!(
963 margin_account
964 .margins
965 .get(&instrument_id_aud_usd_sim)
966 .expect("AUD/USD margin should exist")
967 .initial,
968 Money::zero(margin.currency)
969 );
970 assert_eq!(
972 margin_account.balance_locked(Some(Currency::USD())),
973 Some(Money::from("35000 USD"))
974 );
975 assert_eq!(
976 margin_account.balance_free(Some(Currency::USD())),
977 Some(Money::from("1490000 USD"))
978 );
979 }
980
981 #[rstest]
982 fn test_clear_initial_margin_preserves_maintenance(
983 mut margin_account: MarginAccount,
984 instrument_id_aud_usd_sim: InstrumentId,
985 ) {
986 let usd = Currency::USD();
987 margin_account.update_margin(MarginBalance::new(
988 Money::from("1000 USD"),
989 Money::from("500 USD"),
990 Some(instrument_id_aud_usd_sim),
991 ));
992
993 assert_eq!(
994 margin_account.balance_locked(Some(usd)),
995 Some(Money::from("26500 USD"))
996 );
997
998 margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
999
1000 let margin = margin_account
1001 .margin(&instrument_id_aud_usd_sim)
1002 .expect("margin should retain non-zero maintenance");
1003 assert_eq!(margin.initial, Money::from("0 USD"));
1004 assert_eq!(margin.maintenance, Money::from("500 USD"));
1005 assert_eq!(
1006 margin_account.balance_locked(Some(usd)),
1007 Some(Money::from("25500 USD"))
1008 );
1009 assert_eq!(
1010 margin_account.balance_free(Some(usd)),
1011 Some(Money::from("1499500 USD"))
1012 );
1013 assert_eq!(
1014 margin_account.balance_total(Some(usd)),
1015 Some(Money::from("1525000 USD"))
1016 );
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 let usd = Currency::USD();
1025 margin_account.update_margin(MarginBalance::new(
1026 Money::from("0 USD"),
1027 Money::from("500 USD"),
1028 Some(instrument_id_aud_usd_sim),
1029 ));
1030
1031 assert_eq!(
1032 margin_account.balance_locked(Some(usd)),
1033 Some(Money::from("25500 USD"))
1034 );
1035
1036 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1037
1038 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1039 assert_eq!(
1040 margin_account.balance_locked(Some(usd)),
1041 Some(Money::from("25000 USD"))
1042 );
1043 assert_eq!(
1044 margin_account.balance_free(Some(usd)),
1045 Some(Money::from("1500000 USD"))
1046 );
1047 }
1048
1049 #[rstest]
1050 fn test_clear_maintenance_margin_preserves_initial(
1051 mut margin_account: MarginAccount,
1052 instrument_id_aud_usd_sim: InstrumentId,
1053 ) {
1054 let usd = Currency::USD();
1055 margin_account.update_margin(MarginBalance::new(
1056 Money::from("1000 USD"),
1057 Money::from("500 USD"),
1058 Some(instrument_id_aud_usd_sim),
1059 ));
1060
1061 assert_eq!(
1062 margin_account.balance_locked(Some(usd)),
1063 Some(Money::from("26500 USD"))
1064 );
1065
1066 margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
1067
1068 let margin = margin_account
1069 .margin(&instrument_id_aud_usd_sim)
1070 .expect("margin should retain non-zero initial");
1071 assert_eq!(margin.initial, Money::from("1000 USD"));
1072 assert_eq!(margin.maintenance, Money::from("0 USD"));
1073 assert_eq!(
1074 margin_account.balance_locked(Some(usd)),
1075 Some(Money::from("26000 USD"))
1076 );
1077 assert_eq!(
1078 margin_account.balance_free(Some(usd)),
1079 Some(Money::from("1499000 USD"))
1080 );
1081 }
1082
1083 #[rstest]
1084 fn test_apply_replaces_margin_balances_from_event(
1085 mut margin_account: MarginAccount,
1086 margin_account_state: AccountState,
1087 ) {
1088 let old_instrument_id = margin_account_state.margins[0]
1089 .instrument_id
1090 .expect("stub margin balance carries a concrete instrument_id");
1091 let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1092 let event = AccountState::new(
1093 margin_account_state.account_id,
1094 AccountType::Margin,
1095 margin_account_state.balances.clone(),
1096 vec![MarginBalance::new(
1097 Money::from("12500 USD"),
1098 Money::from("25000 USD"),
1099 Some(new_instrument_id),
1100 )],
1101 true,
1102 uuid4(),
1103 1.into(),
1104 1.into(),
1105 margin_account_state.base_currency,
1106 );
1107
1108 margin_account.apply(event).unwrap();
1109
1110 assert_eq!(
1111 margin_account.initial_margin(new_instrument_id),
1112 Money::from("12500 USD")
1113 );
1114 assert_eq!(
1115 margin_account.maintenance_margin(new_instrument_id),
1116 Money::from("25000 USD")
1117 );
1118 assert!(margin_account.margin(&old_instrument_id).is_none());
1119 }
1120
1121 #[rstest]
1122 fn test_account_type_predicates(margin_account: MarginAccount) {
1123 assert!(!margin_account.is_cash_account());
1124 assert!(margin_account.is_margin_account());
1125 assert!(!Account::is_cash_account(&margin_account));
1126 assert!(Account::is_margin_account(&margin_account));
1127 }
1128
1129 #[rstest]
1130 fn test_equality_compares_account_ids(margin_account_state: AccountState) {
1131 let account = MarginAccount::new(margin_account_state.clone(), true);
1132 let same = MarginAccount::new(margin_account_state.clone(), true);
1133 let mut other_state = margin_account_state;
1134 other_state.account_id = AccountId::from("OTHER-001");
1135 let other = MarginAccount::new(other_state, true);
1136
1137 assert_eq!(account, same);
1138 assert_ne!(account, other);
1139 }
1140
1141 #[rstest]
1142 fn test_apply_routes_account_margins_when_event_has_no_balances(
1143 mut margin_account: MarginAccount,
1144 margin_account_state: AccountState,
1145 ) {
1146 let usd = Currency::USD();
1147
1148 let event = AccountState::new(
1149 margin_account_state.account_id,
1150 AccountType::Margin,
1151 vec![],
1152 vec![MarginBalance::new(
1153 Money::from("12500 USD"),
1154 Money::from("25000 USD"),
1155 None,
1156 )],
1157 true,
1158 uuid4(),
1159 1.into(),
1160 1.into(),
1161 margin_account_state.base_currency,
1162 );
1163
1164 margin_account.apply(event).unwrap();
1165
1166 assert_eq!(
1167 margin_account.account_initial_margins(),
1168 IndexMap::from([(usd, Money::from("12500 USD"))])
1169 );
1170 assert_eq!(
1171 margin_account.account_maintenance_margins(),
1172 IndexMap::from([(usd, Money::from("25000 USD"))])
1173 );
1174 }
1175
1176 #[rstest]
1177 fn test_apply_routes_account_margins_by_currency(
1178 mut margin_account: MarginAccount,
1179 margin_account_state: AccountState,
1180 ) {
1181 let usd = Currency::USD();
1182 let event = AccountState::new(
1183 margin_account_state.account_id,
1184 AccountType::Margin,
1185 margin_account_state.balances.clone(),
1186 vec![MarginBalance::new(
1187 Money::from("12500 USD"),
1188 Money::from("25000 USD"),
1189 None,
1190 )],
1191 true,
1192 uuid4(),
1193 1.into(),
1194 1.into(),
1195 margin_account_state.base_currency,
1196 );
1197
1198 margin_account.apply(event).unwrap();
1199
1200 assert!(margin_account.margins.is_empty());
1201 assert_eq!(margin_account.account_margins.len(), 1);
1202 assert_eq!(
1203 margin_account.account_initial_margin(&usd),
1204 Some(Money::from("12500 USD"))
1205 );
1206 assert_eq!(
1207 margin_account.account_maintenance_margin(&usd),
1208 Some(Money::from("25000 USD"))
1209 );
1210 assert_eq!(
1211 margin_account.total_initial_margin(usd),
1212 Money::from("12500 USD")
1213 );
1214 }
1215
1216 #[rstest]
1217 fn test_apply_empty_event_preserves_margin_balances(
1218 mut margin_account: MarginAccount,
1219 margin_account_state: AccountState,
1220 ) {
1221 let instrument_id = margin_account_state.margins[0]
1222 .instrument_id
1223 .expect("stub margin balance carries a concrete instrument_id");
1224 let initial_margin = margin_account.initial_margin(instrument_id);
1225 let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1226
1227 let empty_event = AccountState::new(
1228 margin_account_state.account_id,
1229 AccountType::Margin,
1230 vec![],
1231 vec![],
1232 true,
1233 uuid4(),
1234 1.into(),
1235 1.into(),
1236 margin_account_state.base_currency,
1237 );
1238
1239 margin_account.apply(empty_event).unwrap();
1240
1241 assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1242 assert_eq!(
1243 margin_account.maintenance_margin(instrument_id),
1244 maintenance_margin
1245 );
1246 assert_eq!(margin_account.event_count(), 2);
1247 }
1248
1249 #[rstest]
1250 fn test_calculate_margin_init_with_leverage(
1251 mut margin_account: MarginAccount,
1252 audusd_sim: CurrencyPair,
1253 ) {
1254 margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1255 let result = margin_account
1256 .calculate_initial_margin(
1257 &audusd_sim,
1258 Quantity::from(100_000),
1259 Price::from("0.8000"),
1260 None,
1261 )
1262 .unwrap();
1263 assert_eq!(result, Money::from("48.00 USD"));
1264 }
1265
1266 #[rstest]
1267 fn test_custom_margin_model_through_account(
1268 mut margin_account: MarginAccount,
1269 audusd_sim: CurrencyPair,
1270 ) {
1271 margin_account.set_margin_model(MarginModelHandle::new(CustomMarginModel));
1272
1273 let initial = margin_account
1274 .calculate_initial_margin(
1275 &audusd_sim,
1276 Quantity::from(100_000),
1277 Price::from("0.8000"),
1278 None,
1279 )
1280 .unwrap();
1281 let maintenance = margin_account
1282 .calculate_maintenance_margin(
1283 &audusd_sim,
1284 Quantity::from(100_000),
1285 Price::from("0.8000"),
1286 None,
1287 )
1288 .unwrap();
1289
1290 assert_eq!(margin_account.margin_model().name(), "custom");
1291 assert_eq!(initial, Money::from("12.34 USD"));
1292 assert_eq!(maintenance, Money::from("5.67 USD"));
1293 }
1294
1295 #[rstest]
1296 fn test_calculate_margin_init_with_default_leverage(
1297 mut margin_account: MarginAccount,
1298 audusd_sim: CurrencyPair,
1299 ) {
1300 margin_account.set_default_leverage(Decimal::from(10));
1301 let result = margin_account
1302 .calculate_initial_margin(
1303 &audusd_sim,
1304 Quantity::from(100_000),
1305 Price::from("0.8"),
1306 None,
1307 )
1308 .unwrap();
1309 assert_eq!(result, Money::from("240.00 USD"));
1310 }
1311
1312 #[rstest]
1313 fn test_calculate_margin_init_with_no_leverage_for_inverse(
1314 margin_account: MarginAccount,
1315 btcusd_bybit: CryptoPerpetual,
1316 ) {
1317 let result_use_quote_inverse_true = margin_account
1318 .calculate_initial_margin(
1319 &btcusd_bybit,
1320 Quantity::from(100_000),
1321 Price::from("11493.60"),
1322 Some(false),
1323 )
1324 .unwrap();
1325 assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1326 let result_use_quote_inverse_false = margin_account
1327 .calculate_initial_margin(
1328 &btcusd_bybit,
1329 Quantity::from(100_000),
1330 Price::from("11493.60"),
1331 Some(true),
1332 )
1333 .unwrap();
1334 assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1335 }
1336
1337 #[rstest]
1338 fn test_calculate_margin_maintenance_with_no_leverage(
1339 margin_account: MarginAccount,
1340 btcusd_bybit: CryptoPerpetual,
1341 ) {
1342 let result = margin_account
1343 .calculate_maintenance_margin(
1344 &btcusd_bybit,
1345 Quantity::from(100_000),
1346 Price::from("11493.60"),
1347 None,
1348 )
1349 .unwrap();
1350 assert_eq!(result, Money::from("0.03045173 BTC"));
1351 }
1352
1353 #[rstest]
1354 fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1355 mut margin_account: MarginAccount,
1356 audusd_sim: CurrencyPair,
1357 ) {
1358 margin_account.set_default_leverage(Decimal::from(50));
1359 let result = margin_account
1360 .calculate_maintenance_margin(
1361 &audusd_sim,
1362 Quantity::from(1_000_000),
1363 Price::from("1"),
1364 None,
1365 )
1366 .unwrap();
1367 assert_eq!(result, Money::from("600.00 USD"));
1368 }
1369
1370 #[rstest]
1371 fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1372 mut margin_account: MarginAccount,
1373 btcusd_bybit: CryptoPerpetual,
1374 ) {
1375 margin_account.set_default_leverage(Decimal::from(10));
1376 let result = margin_account
1377 .calculate_maintenance_margin(
1378 &btcusd_bybit,
1379 Quantity::from(100_000),
1380 Price::from("100000.00"),
1381 None,
1382 )
1383 .unwrap();
1384 assert_eq!(result, Money::from("0.00035000 BTC"));
1385 }
1386
1387 #[rstest]
1388 fn test_calculate_pnls_github_issue_2657() {
1389 let account_state = margin_account_state();
1391 let account = MarginAccount::new(account_state, false);
1392
1393 let btcusdt = currency_pair_btcusdt();
1395 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1396
1397 let fill1 = OrderFilledSpec::builder()
1399 .instrument_id(btcusdt_any.id())
1400 .client_order_id(ClientOrderId::from("O-1"))
1401 .venue_order_id(VenueOrderId::from("V-1"))
1402 .trade_id(TradeId::from("T-1"))
1403 .last_qty(Quantity::from("0.001"))
1404 .last_px(Price::from("50000.00"))
1405 .currency(btcusdt_any.quote_currency())
1406 .ts_event(UnixNanos::from(1_000_000_000))
1407 .position_id(PositionId::from("P-GITHUB-2657"))
1408 .build();
1409
1410 let position = Position::new(&btcusdt_any, fill1);
1411
1412 let fill2 = OrderFilledSpec::builder()
1414 .instrument_id(btcusdt_any.id())
1415 .client_order_id(ClientOrderId::from("O-2"))
1416 .venue_order_id(VenueOrderId::from("V-2"))
1417 .trade_id(TradeId::from("T-2"))
1418 .order_side(OrderSide::Sell)
1419 .last_qty(Quantity::from("0.002")) .last_px(Price::from("50075.00"))
1421 .currency(btcusdt_any.quote_currency())
1422 .ts_event(UnixNanos::from(2_000_000_000))
1423 .position_id(PositionId::from("P-GITHUB-2657"))
1424 .build();
1425
1426 let pnls = account
1428 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1429 .unwrap();
1430
1431 assert_eq!(pnls.len(), 1);
1433
1434 let expected_pnl = Money::from("0.075 USDT");
1437 assert_eq!(pnls[0], expected_pnl);
1438 }
1439
1440 #[rstest]
1441 #[should_panic(expected = "not positive")]
1442 fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1443 margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1444 }
1445
1446 #[rstest]
1447 #[should_panic(expected = "not positive")]
1448 fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1449 margin_account.set_default_leverage(Decimal::ZERO);
1450 }
1451
1452 #[rstest]
1453 #[should_panic(expected = "not positive")]
1454 fn test_set_leverage_negative_panics(
1455 mut margin_account: MarginAccount,
1456 audusd_sim: CurrencyPair,
1457 ) {
1458 margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1459 }
1460
1461 #[rstest]
1462 fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1463 use nautilus_core::UnixNanos;
1464
1465 use crate::{
1466 events::order::spec::OrderFilledSpec,
1467 identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1468 instruments::InstrumentAny,
1469 position::Position,
1470 types::{Price, Quantity},
1471 };
1472
1473 let account_state = margin_account_state();
1475 let account = MarginAccount::new(account_state, false);
1476
1477 let btcusdt = currency_pair_btcusdt();
1479 let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1480
1481 let fill1 = OrderFilledSpec::builder()
1483 .instrument_id(btcusdt.id)
1484 .client_order_id(ClientOrderId::from("O-1"))
1485 .venue_order_id(VenueOrderId::from("V-1"))
1486 .trade_id(TradeId::from("T-1"))
1487 .last_qty(Quantity::from("1.0"))
1488 .last_px(Price::from("50000.00"))
1489 .currency(btcusdt.quote_currency)
1490 .ts_event(UnixNanos::from(1_000_000_000))
1491 .position_id(PositionId::from("P-123456"))
1492 .build();
1493
1494 let position = Position::new(&btcusdt_any, fill1);
1495
1496 let fill2 = OrderFilledSpec::builder()
1498 .instrument_id(btcusdt.id)
1499 .client_order_id(ClientOrderId::from("O-2"))
1500 .venue_order_id(VenueOrderId::from("V-2"))
1501 .trade_id(TradeId::from("T-2"))
1502 .last_qty(Quantity::from("0.5"))
1503 .last_px(Price::from("51000.00"))
1504 .currency(btcusdt.quote_currency)
1505 .ts_event(UnixNanos::from(2_000_000_000))
1506 .position_id(PositionId::from("P-123456"))
1507 .build();
1508
1509 let pnls = account
1511 .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1512 .unwrap();
1513
1514 assert_eq!(pnls.len(), 0);
1516 }
1517
1518 #[cfg(feature = "defi")]
1519 #[rstest]
1520 #[case::precision_16("50001.0000000000000000", None)]
1521 #[case::precision_17("50001.00000000000000000", Some(17))]
1522 #[case::precision_18("50001.000000000000000000", Some(18))]
1523 fn test_calculate_pnls_checks_reducing_fill_price_float_precision(
1524 margin_account: MarginAccount,
1525 #[case] last_px: &str,
1526 #[case] expected_error_precision: Option<u8>,
1527 ) {
1528 let btcusdt = currency_pair_btcusdt();
1529 let instrument = InstrumentAny::CurrencyPair(btcusdt.clone());
1530 let fill_open = OrderFilledSpec::builder()
1531 .instrument_id(btcusdt.id)
1532 .trade_id(TradeId::from("T-1"))
1533 .last_qty(Quantity::from("1.000000"))
1534 .last_px(Price::from("50000.00"))
1535 .currency(btcusdt.quote_currency)
1536 .position_id(PositionId::from("P-1"))
1537 .build();
1538 let position = Position::new(&instrument, fill_open);
1539 let fill_close = OrderFilledSpec::builder()
1540 .instrument_id(btcusdt.id)
1541 .trade_id(TradeId::from("T-2"))
1542 .order_side(OrderSide::Sell)
1543 .last_qty(Quantity::from("1.000000"))
1544 .last_px(Price::from(last_px))
1545 .currency(btcusdt.quote_currency)
1546 .position_id(PositionId::from("P-1"))
1547 .build();
1548
1549 let result = margin_account.calculate_pnls(&instrument, &fill_close, Some(position));
1550
1551 match expected_error_precision {
1552 None => assert_eq!(result.unwrap(), vec![Money::from("1 USDT")]),
1553 Some(precision) => assert_eq!(
1554 result.unwrap_err().to_string(),
1555 format!("Fixed-point precision {precision} exceeds maximum float precision 16")
1556 ),
1557 }
1558 }
1559
1560 #[rstest]
1561 fn test_margin_accessor(
1562 mut margin_account: MarginAccount,
1563 instrument_id_aud_usd_sim: InstrumentId,
1564 ) {
1565 let margin_balance = MarginBalance::new(
1566 Money::from("1000 USD"),
1567 Money::from("500 USD"),
1568 Some(instrument_id_aud_usd_sim),
1569 );
1570
1571 margin_account.update_margin(margin_balance);
1572
1573 let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1574 assert!(retrieved.is_some());
1575 let retrieved = retrieved.unwrap();
1576 assert_eq!(retrieved.initial, Money::from("1000 USD"));
1577 assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1578 assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1579 }
1580
1581 #[rstest]
1582 fn test_clear_margin(
1583 mut margin_account: MarginAccount,
1584 instrument_id_aud_usd_sim: InstrumentId,
1585 ) {
1586 let usd = Currency::USD();
1587 let margin_balance = MarginBalance::new(
1588 Money::from("1000 USD"),
1589 Money::from("500 USD"),
1590 Some(instrument_id_aud_usd_sim),
1591 );
1592
1593 margin_account.update_margin(margin_balance);
1594 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1595 assert_eq!(
1596 margin_account.balance_locked(Some(usd)),
1597 Some(Money::from("26500 USD"))
1598 );
1599 assert_eq!(
1600 margin_account.balance_free(Some(usd)),
1601 Some(Money::from("1498500 USD"))
1602 );
1603
1604 margin_account.clear_margin(instrument_id_aud_usd_sim);
1605
1606 assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1607 assert_eq!(
1608 margin_account.balance_locked(Some(usd)),
1609 Some(Money::from("25000 USD"))
1610 );
1611 assert_eq!(
1612 margin_account.balance_free(Some(usd)),
1613 Some(Money::from("1500000 USD"))
1614 );
1615 assert_eq!(
1616 margin_account.balance_total(Some(usd)),
1617 Some(Money::from("1525000 USD"))
1618 );
1619 }
1620
1621 #[rstest]
1622 fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1623 let usd = Currency::USD();
1624 let margin_balance =
1625 MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1626
1627 margin_account.update_margin(margin_balance);
1628
1629 assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1630 assert_eq!(
1631 margin_account.account_initial_margin(&usd),
1632 Some(Money::from("200 USD"))
1633 );
1634 assert_eq!(
1635 margin_account.account_maintenance_margin(&usd),
1636 Some(Money::from("100 USD"))
1637 );
1638 assert_eq!(
1639 margin_account.balance_locked(Some(usd)),
1640 Some(Money::from("25300 USD"))
1641 );
1642 assert_eq!(
1643 margin_account.balance_free(Some(usd)),
1644 Some(Money::from("1499700 USD"))
1645 );
1646
1647 margin_account.clear_account_margin(usd);
1648
1649 assert!(margin_account.account_margin(&usd).is_none());
1650 assert_eq!(
1651 margin_account.balance_locked(Some(usd)),
1652 Some(Money::from("25000 USD"))
1653 );
1654 assert_eq!(
1655 margin_account.balance_free(Some(usd)),
1656 Some(Money::from("1500000 USD"))
1657 );
1658 }
1659
1660 #[rstest]
1661 #[should_panic(expected = "Cannot get margin_init when no margin_balance")]
1662 fn test_initial_margin_panics_when_absent(
1663 margin_account: MarginAccount,
1664 instrument_id_aud_usd_sim: InstrumentId,
1665 ) {
1666 let _ = margin_account.initial_margin(instrument_id_aud_usd_sim);
1667 }
1668
1669 #[rstest]
1670 #[should_panic(expected = "Cannot get maintenance_margin when no margin_balance")]
1671 fn test_maintenance_margin_panics_when_absent(
1672 margin_account: MarginAccount,
1673 instrument_id_aud_usd_sim: InstrumentId,
1674 ) {
1675 let _ = margin_account.maintenance_margin(instrument_id_aud_usd_sim);
1676 }
1677
1678 fn multi_currency_margin_account(reported: Money) -> MarginAccount {
1681 let state = AccountState::new(
1682 AccountId::from("BYBIT-001"),
1683 AccountType::Margin,
1684 vec![AccountBalance::new(
1685 reported,
1686 Money::zero(reported.currency),
1687 reported,
1688 )],
1689 Vec::new(),
1690 true,
1691 uuid4(),
1692 0.into(),
1693 0.into(),
1694 None,
1695 );
1696 MarginAccount::new(state, true)
1697 }
1698
1699 #[rstest]
1700 fn test_margin_update_leaves_unreported_currency_absent() {
1701 let usdt = Currency::USDT();
1702 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1703 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1704
1705 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1706
1707 assert_eq!(account.balance_total(Some(usdt)), None);
1708 assert_eq!(account.balance_locked(Some(usdt)), None);
1709 assert_eq!(account.balance_free(Some(usdt)), None);
1710 assert_eq!(
1711 account.balances.keys().copied().collect::<Vec<_>>(),
1712 vec![Currency::USD()]
1713 );
1714 assert_eq!(
1715 account.initial_margin(instrument_id),
1716 Money::from("5000 USDT")
1717 );
1718 assert_eq!(
1719 account.balance_free(Some(Currency::USD())),
1720 Some(Money::from("1000000 USD"))
1721 );
1722 }
1723
1724 #[rstest]
1725 fn test_margin_update_locks_reported_currency() {
1726 let usdt = Currency::USDT();
1727 let mut account = multi_currency_margin_account(Money::from("100000 USDT"));
1728 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1729
1730 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1731
1732 assert_eq!(
1733 account.balance_total(Some(usdt)),
1734 Some(Money::from("100000 USDT"))
1735 );
1736 assert_eq!(
1737 account.balance_locked(Some(usdt)),
1738 Some(Money::from("5000 USDT"))
1739 );
1740 assert_eq!(
1741 account.balance_free(Some(usdt)),
1742 Some(Money::from("95000 USDT"))
1743 );
1744 }
1745
1746 #[rstest]
1747 fn test_margin_update_locks_currency_reported_after_the_margin() {
1748 let usdt = Currency::USDT();
1749 let mut account = multi_currency_margin_account(Money::from("1000000 USD"));
1750 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1751
1752 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1753 account
1754 .apply(AccountState::new(
1755 AccountId::from("BYBIT-001"),
1756 AccountType::Margin,
1757 vec![AccountBalance::new(
1758 Money::from("100000 USDT"),
1759 Money::zero(usdt),
1760 Money::from("100000 USDT"),
1761 )],
1762 Vec::new(),
1763 true,
1764 uuid4(),
1765 0.into(),
1766 0.into(),
1767 None,
1768 ))
1769 .unwrap();
1770 account.update_initial_margin(instrument_id, Money::from("5000 USDT"));
1771
1772 assert_eq!(
1773 account.balance_locked(Some(usdt)),
1774 Some(Money::from("5000 USDT"))
1775 );
1776 assert_eq!(
1777 account.balance_free(Some(usdt)),
1778 Some(Money::from("95000 USDT"))
1779 );
1780 }
1781
1782 #[rstest]
1783 fn test_recalculate_balance_clamps_when_margin_exceeds_total() {
1784 let usdt = Currency::USDT();
1785 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1786
1787 account.update_initial_margin(
1788 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
1789 Money::from("1500 USDT"),
1790 );
1791
1792 assert_eq!(
1793 account.balance_total(Some(usdt)),
1794 Some(Money::from("1000 USDT"))
1795 );
1796 assert_eq!(
1797 account.balance_locked(Some(usdt)),
1798 Some(Money::from("1000 USDT"))
1799 );
1800 assert_eq!(
1801 account.balance_free(Some(usdt)),
1802 Some(Money::from("0 USDT"))
1803 );
1804 }
1805
1806 #[rstest]
1807 fn test_recalculate_balance_keeps_locked_non_negative_when_total_negative() {
1808 let usd = Currency::USD();
1809 let mut account = multi_currency_margin_account(Money::from("-1000 USD"));
1810 account.balances.insert(
1812 usd,
1813 AccountBalance::new(
1814 Money::from("-1000 USD"),
1815 Money::from("250 USD"),
1816 Money::from("-1250 USD"),
1817 ),
1818 );
1819
1820 account.update_initial_margin(InstrumentId::from("EURUSD.SIM"), Money::from("500 USD"));
1821
1822 assert_eq!(
1823 account.balance_total(Some(usd)),
1824 Some(Money::from("-1000 USD"))
1825 );
1826 assert_eq!(
1827 account.balance_locked(Some(usd)),
1828 Some(Money::from("0 USD"))
1829 );
1830 assert_eq!(
1831 account.balance_free(Some(usd)),
1832 Some(Money::from("-1000 USD"))
1833 );
1834 }
1835
1836 #[rstest]
1837 fn test_total_margin_sums_per_instrument_and_account_wide(
1838 mut margin_account: MarginAccount,
1839 instrument_id_aud_usd_sim: InstrumentId,
1840 ) {
1841 let usd = Currency::USD();
1842 let baseline_initial = margin_account.total_initial_margin(usd);
1843 let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1844
1845 margin_account.update_margin(MarginBalance::new(
1846 Money::from("100 USD"),
1847 Money::from("50 USD"),
1848 Some(instrument_id_aud_usd_sim),
1849 ));
1850 margin_account.update_margin(MarginBalance::new(
1851 Money::from("200 USD"),
1852 Money::from("150 USD"),
1853 None,
1854 ));
1855
1856 assert_eq!(
1857 margin_account.total_initial_margin(usd),
1858 baseline_initial + Money::from("300 USD"),
1859 );
1860 assert_eq!(
1861 margin_account.total_maintenance_margin(usd),
1862 baseline_maintenance + Money::from("200 USD"),
1863 );
1864 }
1865
1866 #[rstest]
1867 fn test_recalculate_balance_reserves_full_balance_when_margins_cannot_be_totalled() {
1868 let usdt = Currency::USDT();
1869 let mut account = multi_currency_margin_account(Money::from("1000 USDT"));
1870 let max = Money::from_raw(MONEY_RAW_MAX, usdt);
1871
1872 for symbol in ["ETHUSDT-PERP.BINANCE", "BTCUSDT-PERP.BINANCE"] {
1874 let instrument_id = InstrumentId::from(symbol);
1875 account.margins.insert(
1876 instrument_id,
1877 MarginBalance::new(max, max, Some(instrument_id)),
1878 );
1879 }
1880
1881 account.recalculate_balance(usdt);
1882
1883 assert_eq!(
1884 account.balance_total(Some(usdt)),
1885 Some(Money::from("1000 USDT"))
1886 );
1887 assert_eq!(
1888 account.balance_locked(Some(usdt)),
1889 Some(Money::from("1000 USDT"))
1890 );
1891 assert_eq!(
1892 account.balance_free(Some(usdt)),
1893 Some(Money::from("0 USDT"))
1894 );
1895 }
1896
1897 #[rstest]
1898 fn test_total_margin_clamps_domain_overflow(
1899 mut margin_account: MarginAccount,
1900 instrument_id_aud_usd_sim: InstrumentId,
1901 ) {
1902 let usd = Currency::USD();
1903 let max = Money::from_raw(MONEY_RAW_MAX, usd);
1904 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1905
1906 margin_account.margins.insert(
1907 instrument_id_aud_usd_sim,
1908 MarginBalance::new(max, max, Some(instrument_id_aud_usd_sim)),
1909 );
1910 margin_account.margins.insert(
1911 other_instrument,
1912 MarginBalance::new(max, max, Some(other_instrument)),
1913 );
1914
1915 assert_eq!(margin_account.total_initial_margin(usd), max);
1916 assert_eq!(margin_account.total_maintenance_margin(usd), max);
1917 }
1918
1919 #[rstest]
1920 fn test_total_margin_clamps_negative_domain_overflow(
1921 mut margin_account: MarginAccount,
1922 instrument_id_aud_usd_sim: InstrumentId,
1923 ) {
1924 let usd = Currency::USD();
1925 let min = Money::from_raw(MONEY_RAW_MIN, usd);
1926 let other_instrument = InstrumentId::from("EUR/USD.SIM");
1927
1928 margin_account.margins.insert(
1929 instrument_id_aud_usd_sim,
1930 MarginBalance::new(min, min, Some(instrument_id_aud_usd_sim)),
1931 );
1932 margin_account.margins.insert(
1933 other_instrument,
1934 MarginBalance::new(min, min, Some(other_instrument)),
1935 );
1936
1937 assert_eq!(margin_account.total_initial_margin(usd), min);
1938 assert_eq!(margin_account.total_maintenance_margin(usd), min);
1939 }
1940
1941 #[rstest]
1942 fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1943 let option = option_contract_appl();
1944 let option_any = InstrumentAny::OptionContract(option.clone());
1945
1946 let order = OrderTestBuilder::new(OrderType::Market)
1947 .instrument_id(option.id)
1948 .side(OrderSide::Buy)
1949 .quantity(Quantity::from("10"))
1950 .build();
1951
1952 let fill = TestOrderEventStubs::filled(
1953 &order,
1954 &option_any,
1955 None,
1956 Some(PositionId::new("P-OPT-001")),
1957 Some(Price::from("5.50")),
1958 None,
1959 None,
1960 None,
1961 None,
1962 Some(AccountId::from("SIM-001")),
1963 );
1964
1965 let fill_owned: crate::events::OrderFilled = fill.into();
1966 let pnls = margin_account
1967 .calculate_pnls(&option_any, &fill_owned, None)
1968 .unwrap();
1969
1970 assert_eq!(pnls.len(), 1);
1973 assert_eq!(pnls[0], Money::from("-55 USD"));
1974 }
1975
1976 #[rstest]
1977 fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1978 let option = option_contract_appl();
1979 let option_any = InstrumentAny::OptionContract(option.clone());
1980
1981 let order = OrderTestBuilder::new(OrderType::Market)
1982 .instrument_id(option.id)
1983 .side(OrderSide::Sell)
1984 .quantity(Quantity::from("10"))
1985 .build();
1986
1987 let fill = TestOrderEventStubs::filled(
1988 &order,
1989 &option_any,
1990 None,
1991 Some(PositionId::new("P-OPT-002")),
1992 Some(Price::from("5.50")),
1993 None,
1994 None,
1995 None,
1996 None,
1997 Some(AccountId::from("SIM-001")),
1998 );
1999
2000 let fill_owned: crate::events::OrderFilled = fill.into();
2001 let pnls = margin_account
2002 .calculate_pnls(&option_any, &fill_owned, None)
2003 .unwrap();
2004
2005 assert_eq!(pnls.len(), 1);
2008 assert_eq!(pnls[0], Money::from("55 USD"));
2009 }
2010
2011 #[rstest]
2012 #[case(OrderSide::Buy, "-0.002 BTC")]
2013 #[case(OrderSide::Sell, "0.002 BTC")]
2014 fn test_calculate_pnls_for_inverse_option_realizes_premium_in_base(
2015 margin_account: MarginAccount,
2016 mut crypto_option_btc_deribit: CryptoOption,
2017 #[case] side: OrderSide,
2018 #[case] expected: &str,
2019 ) {
2020 crypto_option_btc_deribit.is_inverse = true;
2021 crypto_option_btc_deribit.multiplier = Quantity::from("0.01");
2022 let option = InstrumentAny::CryptoOption(crypto_option_btc_deribit);
2023 let order = OrderTestBuilder::new(OrderType::Market)
2024 .instrument_id(option.id())
2025 .side(side)
2026 .quantity(Quantity::from("10.0"))
2027 .build();
2028 let fill = TestOrderEventStubs::filled(
2029 &order,
2030 &option,
2031 None,
2032 Some(PositionId::new("P-OPT-003")),
2033 Some(Price::from("0.020")),
2034 None,
2035 None,
2036 None,
2037 None,
2038 Some(AccountId::from("SIM-001")),
2039 );
2040
2041 let pnls = margin_account
2042 .calculate_pnls(&option, &fill.into(), None)
2043 .unwrap();
2044
2045 assert_eq!(pnls, vec![Money::from(expected)]);
2046 }
2047
2048 #[rstest]
2049 fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
2050 let binary = binary_option();
2051 let binary_any = InstrumentAny::BinaryOption(binary);
2052
2053 let order = OrderTestBuilder::new(OrderType::Market)
2054 .instrument_id(binary_any.id())
2055 .side(OrderSide::Buy)
2056 .quantity(Quantity::from("100"))
2057 .build();
2058
2059 let fill = TestOrderEventStubs::filled(
2060 &order,
2061 &binary_any,
2062 None,
2063 Some(PositionId::new("P-BIN-001")),
2064 Some(Price::from("0.65")),
2065 None,
2066 None,
2067 None,
2068 None,
2069 Some(AccountId::from("SIM-001")),
2070 );
2071
2072 let fill_owned: crate::events::OrderFilled = fill.into();
2073 let pnls = margin_account
2074 .calculate_pnls(&binary_any, &fill_owned, None)
2075 .unwrap();
2076
2077 assert_eq!(pnls.len(), 1);
2078 assert!(pnls[0].as_f64() < 0.0);
2079 }
2080
2081 #[rstest]
2082 fn test_commission_explicit_schedule_override(
2083 margin_account: MarginAccount,
2084 currency_pair_btcusdt: CurrencyPair,
2085 ) {
2086 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.002), dec!(0.002));
2087 schedule.set_override(
2088 currency_pair_btcusdt.id,
2089 MakerTakerFeeRates::new(dec!(0.0005), dec!(0.0005)),
2090 );
2091 let fee_rates = schedule.rates_for(currency_pair_btcusdt.id);
2092 let result = margin_account
2093 .calculate_commission(
2094 ¤cy_pair_btcusdt.into_any(),
2095 Quantity::from("1"),
2096 Price::from("50000.00"),
2097 LiquiditySide::Maker,
2098 fee_rates,
2099 None,
2100 )
2101 .unwrap();
2102 assert_eq!(result, Money::from("25 USDT"));
2105 }
2106
2107 #[rstest]
2108 fn test_commission_shared_schedule_mutation(
2109 margin_account: MarginAccount,
2110 currency_pair_btcusdt: CurrencyPair,
2111 ) {
2112 let instrument_id = currency_pair_btcusdt.id;
2113 let mut schedule = MakerTakerFeeSchedule::new(dec!(0.001), dec!(0.001));
2114 let before = margin_account
2115 .calculate_commission(
2116 ¤cy_pair_btcusdt.clone().into_any(),
2117 Quantity::from("2"),
2118 Price::from("50000.00"),
2119 LiquiditySide::Taker,
2120 schedule.rates_for(instrument_id),
2121 None,
2122 )
2123 .unwrap();
2124 assert_eq!(before, Money::from("100 USDT"));
2125
2126 schedule.default = MakerTakerFeeRates::new(dec!(0.002), dec!(0.002));
2127 let after = margin_account
2128 .calculate_commission(
2129 ¤cy_pair_btcusdt.into_any(),
2130 Quantity::from("2"),
2131 Price::from("50000.00"),
2132 LiquiditySide::Taker,
2133 schedule.rates_for(instrument_id),
2134 None,
2135 )
2136 .unwrap();
2137 assert_eq!(after, Money::from("200 USDT"));
2138 }
2139}