Skip to main content

nautilus_model/accounts/
margin.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A margin account capable of holding leveraged positions and tracking instrument-specific
17//! leverage ratios.
18//!
19//! # PnL calculation
20//!
21//! The account calculates PnL differently based on instrument type:
22//!
23//! - **Premium instruments** (options, option spreads, binary options, warrants): Realize
24//!   the notional value as a cash flow on every fill. BUY = negative (premium paid),
25//!   SELL = positive (premium received).
26//!
27//! - **Other instruments**: Only realize PnL on position reduction (fill side opposite to
28//!   entry). Use the minimum of fill and position quantity to avoid double-counting.
29
30#![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, MarginModelAny},
49    },
50    enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
51    events::{AccountState, OrderFilled},
52    identifiers::{AccountId, InstrumentId},
53    instruments::{Instrument, InstrumentAny},
54    position::Position,
55    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity, money::MoneyRaw},
56};
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[cfg_attr(
60    feature = "python",
61    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
62)]
63#[cfg_attr(
64    feature = "python",
65    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
66)]
67pub struct MarginAccount {
68    pub base: BaseAccount,
69    pub leverages: AHashMap<InstrumentId, Decimal>,
70    /// Per-instrument margin balances (isolated margin, calculated margin in
71    /// backtest mode). Entries here have a concrete `instrument_id`.
72    pub margins: IndexMap<InstrumentId, MarginBalance>,
73    /// Account-wide (cross margin) margin balances keyed by collateral currency.
74    /// Populated from `AccountState.margins` entries where `instrument_id` is
75    /// `None`. Most derivatives venues in cross-margin mode report here.
76    pub account_margins: IndexMap<Currency, MarginBalance>,
77    pub default_leverage: Decimal,
78    #[serde(skip, default = "MarginModelAny::default")]
79    margin_model: MarginModelAny,
80}
81
82fn split_event_margins(
83    event: &AccountState,
84) -> (
85    IndexMap<InstrumentId, MarginBalance>,
86    IndexMap<Currency, MarginBalance>,
87) {
88    let mut per_instrument: IndexMap<InstrumentId, MarginBalance> = IndexMap::new();
89    let mut per_currency: IndexMap<Currency, MarginBalance> = IndexMap::new();
90
91    for margin in &event.margins {
92        match margin.instrument_id {
93            Some(instrument_id) => {
94                per_instrument.insert(instrument_id, *margin);
95            }
96            None => {
97                per_currency.insert(margin.currency, *margin);
98            }
99        }
100    }
101    (per_instrument, per_currency)
102}
103
104impl MarginAccount {
105    /// Creates a new [`MarginAccount`] instance.
106    #[must_use]
107    pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
108        let (margins, account_margins) = split_event_margins(&event);
109
110        Self {
111            base: BaseAccount::new(event, calculate_account_state),
112            leverages: AHashMap::new(),
113            margins,
114            account_margins,
115            default_leverage: Decimal::ONE,
116            margin_model: MarginModelAny::default(),
117        }
118    }
119
120    pub fn set_margin_model(&mut self, model: MarginModelAny) {
121        self.margin_model = model;
122    }
123
124    #[must_use]
125    pub const fn margin_model(&self) -> &MarginModelAny {
126        &self.margin_model
127    }
128
129    /// Sets the default leverage for the account.
130    ///
131    /// # Panics
132    ///
133    /// Panics if `leverage` is not positive.
134    pub fn set_default_leverage(&mut self, leverage: Decimal) {
135        check_positive_decimal(leverage, "leverage").expect_display(FAILED);
136        self.default_leverage = leverage;
137    }
138
139    /// Sets the leverage for a specific instrument.
140    ///
141    /// # Panics
142    ///
143    /// Panics if `leverage` is not positive.
144    pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
145        check_positive_decimal(leverage, "leverage").expect_display(FAILED);
146        self.leverages.insert(instrument_id, leverage);
147    }
148
149    #[must_use]
150    pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
151        *self
152            .leverages
153            .get(instrument_id)
154            .unwrap_or(&self.default_leverage)
155    }
156
157    #[must_use]
158    pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
159        self.get_leverage(&instrument_id) == Decimal::ONE
160    }
161
162    #[must_use]
163    pub fn is_cash_account(&self) -> bool {
164        self.account_type == AccountType::Cash
165    }
166
167    #[must_use]
168    pub fn is_margin_account(&self) -> bool {
169        self.account_type == AccountType::Margin
170    }
171
172    #[must_use]
173    pub fn initial_margins(&self) -> IndexMap<InstrumentId, Money> {
174        self.margins
175            .values()
176            .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.initial)))
177            .collect()
178    }
179
180    #[must_use]
181    pub fn maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
182        self.margins
183            .values()
184            .filter_map(|margin| margin.instrument_id.map(|id| (id, margin.maintenance)))
185            .collect()
186    }
187
188    /// Returns all account-wide initial margins keyed by currency.
189    #[must_use]
190    pub fn account_initial_margins(&self) -> IndexMap<Currency, Money> {
191        self.account_margins
192            .values()
193            .map(|margin| (margin.currency, margin.initial))
194            .collect()
195    }
196
197    /// Returns all account-wide maintenance margins keyed by currency.
198    #[must_use]
199    pub fn account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
200        self.account_margins
201            .values()
202            .map(|margin| (margin.currency, margin.maintenance))
203            .collect()
204    }
205
206    /// Updates the initial margin for the specified instrument.
207    pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
208        let margin_balance = self.margins.get(&instrument_id);
209        if let Some(balance) = margin_balance {
210            // update the margin_balance initial property with margin_init
211            let mut new_margin_balance = *balance;
212            new_margin_balance.initial = margin_init;
213            self.margins.insert(instrument_id, new_margin_balance);
214        } else {
215            self.margins.insert(
216                instrument_id,
217                MarginBalance::new(
218                    margin_init,
219                    Money::zero(margin_init.currency),
220                    Some(instrument_id),
221                ),
222            );
223        }
224        self.recalculate_balance(margin_init.currency);
225    }
226
227    /// Clears the initial margin for the specified instrument.
228    pub fn clear_initial_margin(&mut self, instrument_id: InstrumentId) {
229        let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
230            return;
231        };
232
233        if margin_balance.maintenance.is_zero() {
234            self.margins.shift_remove(&instrument_id);
235        } else {
236            let mut new_margin_balance = margin_balance;
237            new_margin_balance.initial = Money::zero(margin_balance.currency);
238            self.margins.insert(instrument_id, new_margin_balance);
239        }
240
241        self.recalculate_balance(margin_balance.currency);
242    }
243
244    /// Returns the initial margin amount for the specified instrument.
245    ///
246    /// # Panics
247    ///
248    /// Panics if no margin balance exists for the given `instrument_id`.
249    #[must_use]
250    pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
251        let margin_balance = self.margins.get(&instrument_id);
252        assert!(
253            margin_balance.is_some(),
254            "Cannot get margin_init when no margin_balance"
255        );
256        margin_balance.unwrap().initial
257    }
258
259    /// Updates the maintenance margin for the specified instrument.
260    pub fn update_maintenance_margin(
261        &mut self,
262        instrument_id: InstrumentId,
263        margin_maintenance: Money,
264    ) {
265        let margin_balance = self.margins.get(&instrument_id);
266        if let Some(balance) = margin_balance {
267            // update the margin_balance maintenance property with margin_maintenance
268            let mut new_margin_balance = *balance;
269            new_margin_balance.maintenance = margin_maintenance;
270            self.margins.insert(instrument_id, new_margin_balance);
271        } else {
272            self.margins.insert(
273                instrument_id,
274                MarginBalance::new(
275                    Money::zero(margin_maintenance.currency),
276                    margin_maintenance,
277                    Some(instrument_id),
278                ),
279            );
280        }
281        self.recalculate_balance(margin_maintenance.currency);
282    }
283
284    /// Clears the maintenance margin for the specified instrument.
285    pub fn clear_maintenance_margin(&mut self, instrument_id: InstrumentId) {
286        let Some(margin_balance) = self.margins.get(&instrument_id).copied() else {
287            return;
288        };
289
290        if margin_balance.initial.is_zero() {
291            self.margins.shift_remove(&instrument_id);
292        } else {
293            let mut new_margin_balance = margin_balance;
294            new_margin_balance.maintenance = Money::zero(margin_balance.currency);
295            self.margins.insert(instrument_id, new_margin_balance);
296        }
297
298        self.recalculate_balance(margin_balance.currency);
299    }
300
301    /// Returns the maintenance margin amount for the specified instrument.
302    ///
303    /// # Panics
304    ///
305    /// Panics if no margin balance exists for the given `instrument_id`.
306    #[must_use]
307    pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
308        let margin_balance = self.margins.get(&instrument_id);
309        assert!(
310            margin_balance.is_some(),
311            "Cannot get maintenance_margin when no margin_balance"
312        );
313        margin_balance.unwrap().maintenance
314    }
315
316    /// Returns the margin balance for the specified instrument.
317    #[must_use]
318    pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
319        self.margins.get(instrument_id).copied()
320    }
321
322    /// Returns the account-wide margin balance for the specified collateral currency.
323    #[must_use]
324    pub fn account_margin(&self, currency: &Currency) -> Option<MarginBalance> {
325        self.account_margins.get(currency).copied()
326    }
327
328    /// Returns the account-wide initial margin for the specified collateral currency.
329    #[must_use]
330    pub fn account_initial_margin(&self, currency: &Currency) -> Option<Money> {
331        self.account_margins.get(currency).map(|m| m.initial)
332    }
333
334    /// Returns the account-wide maintenance margin for the specified collateral currency.
335    #[must_use]
336    pub fn account_maintenance_margin(&self, currency: &Currency) -> Option<Money> {
337        self.account_margins.get(currency).map(|m| m.maintenance)
338    }
339
340    /// Returns the total initial margin reserved in the specified currency,
341    /// summing per-instrument and account-wide entries.
342    #[must_use]
343    pub fn total_initial_margin(&self, currency: Currency) -> Money {
344        let mut raw: MoneyRaw = 0;
345
346        for margin in self.margins.values() {
347            if margin.currency == currency {
348                raw = raw.saturating_add(margin.initial.raw);
349            }
350        }
351
352        for margin in self.account_margins.values() {
353            if margin.currency == currency {
354                raw = raw.saturating_add(margin.initial.raw);
355            }
356        }
357
358        Money::from_raw(raw, currency)
359    }
360
361    /// Returns the total maintenance margin reserved in the specified currency,
362    /// summing per-instrument and account-wide entries.
363    #[must_use]
364    pub fn total_maintenance_margin(&self, currency: Currency) -> Money {
365        let mut raw: MoneyRaw = 0;
366
367        for margin in self.margins.values() {
368            if margin.currency == currency {
369                raw = raw.saturating_add(margin.maintenance.raw);
370            }
371        }
372
373        for margin in self.account_margins.values() {
374            if margin.currency == currency {
375                raw = raw.saturating_add(margin.maintenance.raw);
376            }
377        }
378
379        Money::from_raw(raw, currency)
380    }
381
382    /// Updates the margin balance for the specified instrument or collateral.
383    ///
384    /// When `margin_balance.instrument_id` is `Some`, the entry is stored as a
385    /// per-instrument margin. When `None`, the entry is stored as an
386    /// account-wide margin keyed by `margin_balance.currency`.
387    pub fn update_margin(&mut self, margin_balance: MarginBalance) {
388        match margin_balance.instrument_id {
389            Some(instrument_id) => {
390                self.margins.insert(instrument_id, margin_balance);
391            }
392            None => {
393                self.account_margins
394                    .insert(margin_balance.currency, margin_balance);
395            }
396        }
397        self.recalculate_balance(margin_balance.currency);
398    }
399
400    /// Clears the margin for the specified instrument.
401    pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
402        if let Some(margin_balance) = self.margins.shift_remove(&instrument_id) {
403            self.recalculate_balance(margin_balance.currency);
404        }
405    }
406
407    /// Clears the account-wide margin for the specified collateral currency.
408    pub fn clear_account_margin(&mut self, currency: Currency) {
409        if self.account_margins.shift_remove(&currency).is_some() {
410            self.recalculate_balance(currency);
411        }
412    }
413
414    /// Calculates the initial margin amount for the specified instrument and quantity.
415    ///
416    /// Delegates to the configured [`MarginModel`].
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if leverage is not positive, or if the result cannot be represented
421    /// as `Money`.
422    pub fn calculate_initial_margin<T: Instrument>(
423        &mut self,
424        instrument: &T,
425        quantity: Quantity,
426        price: Price,
427        use_quote_for_inverse: Option<bool>,
428    ) -> anyhow::Result<Money> {
429        let leverage = self.get_leverage(&instrument.id());
430        self.margin_model.calculate_initial_margin(
431            instrument,
432            quantity,
433            price,
434            leverage,
435            use_quote_for_inverse,
436        )
437    }
438
439    /// Calculates the maintenance margin amount for the specified instrument and quantity.
440    ///
441    /// Delegates to the configured [`MarginModel`].
442    ///
443    /// # Errors
444    ///
445    /// Returns an error if the result cannot be represented as `Money`.
446    pub fn calculate_maintenance_margin<T: Instrument>(
447        &mut self,
448        instrument: &T,
449        quantity: Quantity,
450        price: Price,
451        use_quote_for_inverse: Option<bool>,
452    ) -> anyhow::Result<Money> {
453        let leverage = self.get_leverage(&instrument.id());
454        self.margin_model.calculate_maintenance_margin(
455            instrument,
456            quantity,
457            price,
458            leverage,
459            use_quote_for_inverse,
460        )
461    }
462
463    /// Recalculates the account balance for the specified currency based on current margins.
464    ///
465    /// # Panics
466    ///
467    /// This function panics if:
468    /// - Margin calculation overflows.
469    pub fn recalculate_balance(&mut self, currency: Currency) {
470        let current_balance = if let Some(balance) = self.balances.get(&currency) {
471            *balance
472        } else {
473            // Initialize zero balance if none exists - can occur when account
474            // state doesn't include a balance for the position's cost currency
475            let zero = Money::from_raw(0, currency);
476            AccountBalance::new(zero, zero, zero)
477        };
478
479        let mut total_margin: MoneyRaw = 0;
480
481        let accumulate = |raw: MoneyRaw, margin: &MarginBalance| -> MoneyRaw {
482            raw.checked_add(margin.initial.raw)
483                .and_then(|sum| sum.checked_add(margin.maintenance.raw))
484                .unwrap_or_else(|| {
485                    panic!(
486                        "Margin calculation overflow for currency {}: total would exceed maximum",
487                        currency.code
488                    )
489                })
490        };
491
492        for margin in self.margins.values() {
493            if margin.currency == currency {
494                total_margin = accumulate(total_margin, margin);
495            }
496        }
497
498        for margin in self.account_margins.values() {
499            if margin.currency == currency {
500                total_margin = accumulate(total_margin, margin);
501            }
502        }
503
504        // Clamp margin to total balance if it would result in negative free balance.
505        // This can occur transiently when venue and client state are out of sync.
506        // Locked margin must never be negative (even if total balance is negative).
507        let total_free = if total_margin > current_balance.total.raw {
508            total_margin = current_balance.total.raw.max(0);
509            current_balance.total.raw - total_margin
510        } else {
511            current_balance.total.raw - total_margin
512        };
513
514        let new_balance = AccountBalance::new(
515            current_balance.total,
516            Money::from_raw(total_margin, currency),
517            Money::from_raw(total_free, currency),
518        );
519        self.balances.insert(currency, new_balance);
520    }
521}
522
523impl Deref for MarginAccount {
524    type Target = BaseAccount;
525
526    fn deref(&self) -> &Self::Target {
527        &self.base
528    }
529}
530
531impl DerefMut for MarginAccount {
532    fn deref_mut(&mut self) -> &mut Self::Target {
533        &mut self.base
534    }
535}
536
537impl Account for MarginAccount {
538    fn id(&self) -> AccountId {
539        self.id
540    }
541
542    fn account_type(&self) -> AccountType {
543        self.account_type
544    }
545
546    fn base_currency(&self) -> Option<Currency> {
547        self.base_currency
548    }
549
550    fn is_cash_account(&self) -> bool {
551        self.account_type == AccountType::Cash
552    }
553
554    fn is_margin_account(&self) -> bool {
555        self.account_type == AccountType::Margin
556    }
557
558    fn calculated_account_state(&self) -> bool {
559        self.calculate_account_state
560    }
561
562    fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
563        self.base_balance_total(currency)
564    }
565
566    fn balances_total(&self) -> IndexMap<Currency, Money> {
567        self.base_balances_total()
568    }
569
570    fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
571        self.base_balance_free(currency)
572    }
573
574    fn balances_free(&self) -> IndexMap<Currency, Money> {
575        self.base_balances_free()
576    }
577
578    fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
579        self.base_balance_locked(currency)
580    }
581
582    fn balances_locked(&self) -> IndexMap<Currency, Money> {
583        self.base_balances_locked()
584    }
585
586    fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
587        self.base_balance(currency)
588    }
589
590    fn last_event(&self) -> Option<AccountState> {
591        self.base_last_event()
592    }
593
594    fn events(&self) -> Vec<AccountState> {
595        self.events.clone()
596    }
597
598    fn event_count(&self) -> usize {
599        self.events.len()
600    }
601
602    fn currencies(&self) -> Vec<Currency> {
603        self.balances.keys().copied().collect()
604    }
605
606    fn starting_balances(&self) -> IndexMap<Currency, Money> {
607        self.balances_starting.clone()
608    }
609
610    fn balances(&self) -> IndexMap<Currency, AccountBalance> {
611        self.balances.clone()
612    }
613
614    fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
615        let skip_margin_routing = event.balances.is_empty() && event.margins.is_empty();
616        let (per_instrument, per_currency) = split_event_margins(&event);
617        self.base_apply(event);
618
619        if !skip_margin_routing {
620            self.margins = per_instrument;
621            self.account_margins = per_currency;
622        }
623        Ok(())
624    }
625
626    fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
627        self.base.base_purge_account_events(ts_now, lookback_secs);
628    }
629
630    fn calculate_balance_locked(
631        &mut self,
632        instrument: &InstrumentAny,
633        side: OrderSide,
634        quantity: Quantity,
635        price: Price,
636        use_quote_for_inverse: Option<bool>,
637    ) -> anyhow::Result<Money> {
638        self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
639    }
640
641    fn calculate_pnls(
642        &self,
643        instrument: &InstrumentAny,
644        fill: &OrderFilled,
645        position: Option<Position>,
646    ) -> anyhow::Result<Vec<Money>> {
647        let mut pnls: Vec<Money> = Vec::new();
648
649        // For premium-based instruments, realize the notional value as a cash flow on every fill
650        let instrument_class = instrument.instrument_class();
651
652        if matches!(
653            instrument_class,
654            InstrumentClass::Option
655                | InstrumentClass::OptionSpread
656                | InstrumentClass::BinaryOption
657                | InstrumentClass::Warrant
658        ) {
659            let notional = instrument.calculate_notional_value(fill.last_qty, fill.last_px, None);
660            let pnl = if fill.order_side == OrderSide::Buy {
661                Money::from_raw(-notional.raw, notional.currency)
662            } else {
663                notional
664            };
665            pnls.push(pnl);
666            return Ok(pnls);
667        }
668
669        // For other instruments, only realize PnL on position reduction
670        if let Some(ref pos) = position
671            && pos.quantity.is_positive()
672            && pos.entry != fill.order_side
673        {
674            // Calculate and add PnL using the minimum of fill quantity and position quantity
675            // to avoid double-limiting that occurs in position.calculate_pnl()
676            let pnl_quantity = Quantity::from_raw(
677                fill.last_qty.raw.min(pos.quantity.raw),
678                fill.last_qty.precision,
679            );
680            let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
681            pnls.push(pnl);
682        }
683
684        Ok(pnls)
685    }
686
687    fn calculate_commission(
688        &self,
689        instrument: &InstrumentAny,
690        last_qty: Quantity,
691        last_px: Price,
692        liquidity_side: LiquiditySide,
693        use_quote_for_inverse: Option<bool>,
694    ) -> anyhow::Result<Money> {
695        self.base_calculate_commission(
696            instrument,
697            last_qty,
698            last_px,
699            liquidity_side,
700            use_quote_for_inverse,
701        )
702    }
703}
704
705impl PartialEq for MarginAccount {
706    fn eq(&self, other: &Self) -> bool {
707        self.id == other.id
708    }
709}
710
711impl Eq for MarginAccount {}
712
713impl Display for MarginAccount {
714    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715        write!(
716            f,
717            "MarginAccount(id={}, type={}, base={})",
718            self.id,
719            self.account_type,
720            self.base_currency.map_or_else(
721                || "None".to_string(),
722                |base_currency| format!("{}", base_currency.code)
723            ),
724        )
725    }
726}
727
728impl Hash for MarginAccount {
729    fn hash<H: Hasher>(&self, state: &mut H) {
730        self.id.hash(state);
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use indexmap::IndexMap;
737    use nautilus_core::UnixNanos;
738    use rstest::rstest;
739    use rust_decimal::Decimal;
740
741    use crate::{
742        accounts::{Account, MarginAccount, stubs::*},
743        enums::{AccountType, OrderSide, OrderType},
744        events::{AccountState, account::stubs::*, order::spec::OrderFilledSpec},
745        identifiers::{
746            AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId,
747            stubs::{uuid4, *},
748        },
749        instruments::{
750            CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
751            stubs::{binary_option, option_contract_appl, *},
752        },
753        orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
754        position::Position,
755        types::{Currency, MarginBalance, Money, Price, Quantity},
756    };
757
758    #[rstest]
759    fn test_display(margin_account: MarginAccount) {
760        assert_eq!(
761            margin_account.to_string(),
762            "MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
763        );
764    }
765
766    #[rstest]
767    fn test_calculated_account_state_returns_field_value(margin_account_state: AccountState) {
768        assert!(MarginAccount::new(margin_account_state.clone(), true).calculated_account_state());
769        assert!(!MarginAccount::new(margin_account_state, false).calculated_account_state());
770    }
771
772    #[rstest]
773    fn test_base_account_properties(
774        margin_account: MarginAccount,
775        margin_account_state: AccountState,
776    ) {
777        assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
778        assert_eq!(
779            margin_account.last_event(),
780            Some(margin_account_state.clone())
781        );
782        assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
783        assert_eq!(margin_account.event_count(), 1);
784        assert_eq!(
785            margin_account.balance_total(None),
786            Some(Money::from("1525000 USD"))
787        );
788        assert_eq!(
789            margin_account.balance_free(None),
790            Some(Money::from("1500000 USD"))
791        );
792        assert_eq!(
793            margin_account.balance_locked(None),
794            Some(Money::from("25000 USD"))
795        );
796        let mut balances_total_expected = IndexMap::new();
797        balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
798        assert_eq!(margin_account.balances_total(), balances_total_expected);
799        let mut balances_free_expected = IndexMap::new();
800        balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
801        assert_eq!(margin_account.balances_free(), balances_free_expected);
802        let mut balances_locked_expected = IndexMap::new();
803        balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
804        assert_eq!(margin_account.balances_locked(), balances_locked_expected);
805        let margin_balance = margin_account_state.margins[0];
806        let instrument_id = margin_balance
807            .instrument_id
808            .expect("stub margin balance carries a concrete instrument_id");
809        let mut initial_margins_expected = IndexMap::new();
810        initial_margins_expected.insert(instrument_id, margin_balance.initial);
811        assert_eq!(margin_account.initial_margins(), initial_margins_expected);
812        let mut maintenance_margins_expected = IndexMap::new();
813        maintenance_margins_expected.insert(instrument_id, margin_balance.maintenance);
814        assert_eq!(
815            margin_account.maintenance_margins(),
816            maintenance_margins_expected
817        );
818    }
819
820    #[rstest]
821    fn test_set_default_leverage(mut margin_account: MarginAccount) {
822        assert_eq!(margin_account.default_leverage, Decimal::ONE);
823        margin_account.set_default_leverage(Decimal::from(10));
824        assert_eq!(margin_account.default_leverage, Decimal::from(10));
825    }
826
827    #[rstest]
828    fn test_get_leverage_default_leverage(
829        margin_account: MarginAccount,
830        instrument_id_aud_usd_sim: InstrumentId,
831    ) {
832        assert_eq!(
833            margin_account.get_leverage(&instrument_id_aud_usd_sim),
834            Decimal::ONE
835        );
836    }
837
838    #[rstest]
839    fn test_set_leverage(
840        mut margin_account: MarginAccount,
841        instrument_id_aud_usd_sim: InstrumentId,
842    ) {
843        assert_eq!(margin_account.leverages.len(), 0);
844        margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
845        assert_eq!(margin_account.leverages.len(), 1);
846        assert_eq!(
847            margin_account.get_leverage(&instrument_id_aud_usd_sim),
848            Decimal::from(10)
849        );
850    }
851
852    #[rstest]
853    fn test_is_unleveraged_with_leverage_returns_false(
854        mut margin_account: MarginAccount,
855        instrument_id_aud_usd_sim: InstrumentId,
856    ) {
857        margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
858        assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
859    }
860
861    #[rstest]
862    fn test_is_unleveraged_with_no_leverage_returns_true(
863        mut margin_account: MarginAccount,
864        instrument_id_aud_usd_sim: InstrumentId,
865    ) {
866        margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
867        assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
868    }
869
870    #[rstest]
871    fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
872        margin_account: MarginAccount,
873        instrument_id_aud_usd_sim: InstrumentId,
874    ) {
875        assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
876    }
877
878    #[rstest]
879    fn test_update_margin_init(
880        mut margin_account: MarginAccount,
881        instrument_id_aud_usd_sim: InstrumentId,
882    ) {
883        assert_eq!(margin_account.margins.len(), 1);
884        let margin = Money::from("10000 USD");
885        margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
886        assert_eq!(
887            margin_account.initial_margin(instrument_id_aud_usd_sim),
888            margin
889        );
890        assert_eq!(margin_account.margins.len(), 2);
891        assert_eq!(
892            margin_account
893                .margins
894                .get(&instrument_id_aud_usd_sim)
895                .expect("AUD/USD margin should exist")
896                .initial,
897            margin
898        );
899        assert_eq!(
900            margin_account
901                .margins
902                .get(&instrument_id_aud_usd_sim)
903                .expect("AUD/USD margin should exist")
904                .maintenance,
905            Money::zero(margin.currency)
906        );
907    }
908
909    #[rstest]
910    fn test_update_margin_maintenance(
911        mut margin_account: MarginAccount,
912        instrument_id_aud_usd_sim: InstrumentId,
913    ) {
914        let margin = Money::from("10000 USD");
915        margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
916        assert_eq!(
917            margin_account.maintenance_margin(instrument_id_aud_usd_sim),
918            margin
919        );
920        assert_eq!(margin_account.margins.len(), 2);
921        assert_eq!(
922            margin_account
923                .margins
924                .get(&instrument_id_aud_usd_sim)
925                .expect("AUD/USD margin should exist")
926                .maintenance,
927            margin
928        );
929        assert_eq!(
930            margin_account
931                .margins
932                .get(&instrument_id_aud_usd_sim)
933                .expect("AUD/USD margin should exist")
934                .initial,
935            Money::zero(margin.currency)
936        );
937    }
938
939    #[rstest]
940    fn test_clear_initial_margin_preserves_maintenance(
941        mut margin_account: MarginAccount,
942        instrument_id_aud_usd_sim: InstrumentId,
943    ) {
944        margin_account.update_margin(MarginBalance::new(
945            Money::from("1000 USD"),
946            Money::from("500 USD"),
947            Some(instrument_id_aud_usd_sim),
948        ));
949
950        margin_account.clear_initial_margin(instrument_id_aud_usd_sim);
951
952        let margin = margin_account
953            .margin(&instrument_id_aud_usd_sim)
954            .expect("margin should retain non-zero maintenance");
955        assert_eq!(margin.initial, Money::from("0 USD"));
956        assert_eq!(margin.maintenance, Money::from("500 USD"));
957    }
958
959    #[rstest]
960    fn test_clear_maintenance_margin_removes_empty_entry(
961        mut margin_account: MarginAccount,
962        instrument_id_aud_usd_sim: InstrumentId,
963    ) {
964        margin_account.update_margin(MarginBalance::new(
965            Money::from("0 USD"),
966            Money::from("500 USD"),
967            Some(instrument_id_aud_usd_sim),
968        ));
969
970        margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
971
972        assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
973    }
974
975    #[rstest]
976    fn test_clear_maintenance_margin_preserves_initial(
977        mut margin_account: MarginAccount,
978        instrument_id_aud_usd_sim: InstrumentId,
979    ) {
980        margin_account.update_margin(MarginBalance::new(
981            Money::from("1000 USD"),
982            Money::from("500 USD"),
983            Some(instrument_id_aud_usd_sim),
984        ));
985
986        margin_account.clear_maintenance_margin(instrument_id_aud_usd_sim);
987
988        let margin = margin_account
989            .margin(&instrument_id_aud_usd_sim)
990            .expect("margin should retain non-zero initial");
991        assert_eq!(margin.initial, Money::from("1000 USD"));
992        assert_eq!(margin.maintenance, Money::from("0 USD"));
993    }
994
995    #[rstest]
996    fn test_apply_replaces_margin_balances_from_event(
997        mut margin_account: MarginAccount,
998        margin_account_state: AccountState,
999    ) {
1000        let old_instrument_id = margin_account_state.margins[0]
1001            .instrument_id
1002            .expect("stub margin balance carries a concrete instrument_id");
1003        let new_instrument_id = InstrumentId::from("USDJPY.SIM");
1004        let event = AccountState::new(
1005            margin_account_state.account_id,
1006            AccountType::Margin,
1007            margin_account_state.balances.clone(),
1008            vec![MarginBalance::new(
1009                Money::from("12500 USD"),
1010                Money::from("25000 USD"),
1011                Some(new_instrument_id),
1012            )],
1013            true,
1014            uuid4(),
1015            1.into(),
1016            1.into(),
1017            margin_account_state.base_currency,
1018        );
1019
1020        margin_account.apply(event).unwrap();
1021
1022        assert_eq!(
1023            margin_account.initial_margin(new_instrument_id),
1024            Money::from("12500 USD")
1025        );
1026        assert_eq!(
1027            margin_account.maintenance_margin(new_instrument_id),
1028            Money::from("25000 USD")
1029        );
1030        assert!(margin_account.margin(&old_instrument_id).is_none());
1031    }
1032
1033    #[rstest]
1034    fn test_apply_routes_account_margins_by_currency(
1035        mut margin_account: MarginAccount,
1036        margin_account_state: AccountState,
1037    ) {
1038        let usd = Currency::USD();
1039        let event = AccountState::new(
1040            margin_account_state.account_id,
1041            AccountType::Margin,
1042            margin_account_state.balances.clone(),
1043            vec![MarginBalance::new(
1044                Money::from("12500 USD"),
1045                Money::from("25000 USD"),
1046                None,
1047            )],
1048            true,
1049            uuid4(),
1050            1.into(),
1051            1.into(),
1052            margin_account_state.base_currency,
1053        );
1054
1055        margin_account.apply(event).unwrap();
1056
1057        assert!(margin_account.margins.is_empty());
1058        assert_eq!(margin_account.account_margins.len(), 1);
1059        assert_eq!(
1060            margin_account.account_initial_margin(&usd),
1061            Some(Money::from("12500 USD"))
1062        );
1063        assert_eq!(
1064            margin_account.account_maintenance_margin(&usd),
1065            Some(Money::from("25000 USD"))
1066        );
1067        assert_eq!(
1068            margin_account.total_initial_margin(usd),
1069            Money::from("12500 USD")
1070        );
1071    }
1072
1073    #[rstest]
1074    fn test_apply_empty_event_preserves_margin_balances(
1075        mut margin_account: MarginAccount,
1076        margin_account_state: AccountState,
1077    ) {
1078        let instrument_id = margin_account_state.margins[0]
1079            .instrument_id
1080            .expect("stub margin balance carries a concrete instrument_id");
1081        let initial_margin = margin_account.initial_margin(instrument_id);
1082        let maintenance_margin = margin_account.maintenance_margin(instrument_id);
1083
1084        let empty_event = AccountState::new(
1085            margin_account_state.account_id,
1086            AccountType::Margin,
1087            vec![],
1088            vec![],
1089            true,
1090            uuid4(),
1091            1.into(),
1092            1.into(),
1093            margin_account_state.base_currency,
1094        );
1095
1096        margin_account.apply(empty_event).unwrap();
1097
1098        assert_eq!(margin_account.initial_margin(instrument_id), initial_margin);
1099        assert_eq!(
1100            margin_account.maintenance_margin(instrument_id),
1101            maintenance_margin
1102        );
1103        assert_eq!(margin_account.event_count(), 2);
1104    }
1105
1106    #[rstest]
1107    fn test_calculate_margin_init_with_leverage(
1108        mut margin_account: MarginAccount,
1109        audusd_sim: CurrencyPair,
1110    ) {
1111        margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
1112        let result = margin_account
1113            .calculate_initial_margin(
1114                &audusd_sim,
1115                Quantity::from(100_000),
1116                Price::from("0.8000"),
1117                None,
1118            )
1119            .unwrap();
1120        assert_eq!(result, Money::from("48.00 USD"));
1121    }
1122
1123    #[rstest]
1124    fn test_calculate_margin_init_with_default_leverage(
1125        mut margin_account: MarginAccount,
1126        audusd_sim: CurrencyPair,
1127    ) {
1128        margin_account.set_default_leverage(Decimal::from(10));
1129        let result = margin_account
1130            .calculate_initial_margin(
1131                &audusd_sim,
1132                Quantity::from(100_000),
1133                Price::from("0.8"),
1134                None,
1135            )
1136            .unwrap();
1137        assert_eq!(result, Money::from("240.00 USD"));
1138    }
1139
1140    #[rstest]
1141    fn test_calculate_margin_init_with_no_leverage_for_inverse(
1142        mut margin_account: MarginAccount,
1143        xbtusd_bitmex: CryptoPerpetual,
1144    ) {
1145        let result_use_quote_inverse_true = margin_account
1146            .calculate_initial_margin(
1147                &xbtusd_bitmex,
1148                Quantity::from(100_000),
1149                Price::from("11493.60"),
1150                Some(false),
1151            )
1152            .unwrap();
1153        assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
1154        let result_use_quote_inverse_false = margin_account
1155            .calculate_initial_margin(
1156                &xbtusd_bitmex,
1157                Quantity::from(100_000),
1158                Price::from("11493.60"),
1159                Some(true),
1160            )
1161            .unwrap();
1162        assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
1163    }
1164
1165    #[rstest]
1166    fn test_calculate_margin_maintenance_with_no_leverage(
1167        mut margin_account: MarginAccount,
1168        xbtusd_bitmex: CryptoPerpetual,
1169    ) {
1170        let result = margin_account
1171            .calculate_maintenance_margin(
1172                &xbtusd_bitmex,
1173                Quantity::from(100_000),
1174                Price::from("11493.60"),
1175                None,
1176            )
1177            .unwrap();
1178        assert_eq!(result, Money::from("0.03045173 BTC"));
1179    }
1180
1181    #[rstest]
1182    fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
1183        mut margin_account: MarginAccount,
1184        audusd_sim: CurrencyPair,
1185    ) {
1186        margin_account.set_default_leverage(Decimal::from(50));
1187        let result = margin_account
1188            .calculate_maintenance_margin(
1189                &audusd_sim,
1190                Quantity::from(1_000_000),
1191                Price::from("1"),
1192                None,
1193            )
1194            .unwrap();
1195        assert_eq!(result, Money::from("600.00 USD"));
1196    }
1197
1198    #[rstest]
1199    fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
1200        mut margin_account: MarginAccount,
1201        xbtusd_bitmex: CryptoPerpetual,
1202    ) {
1203        margin_account.set_default_leverage(Decimal::from(10));
1204        let result = margin_account
1205            .calculate_maintenance_margin(
1206                &xbtusd_bitmex,
1207                Quantity::from(100_000),
1208                Price::from("100000.00"),
1209                None,
1210            )
1211            .unwrap();
1212        assert_eq!(result, Money::from("0.00035000 BTC"));
1213    }
1214
1215    #[rstest]
1216    fn test_calculate_pnls_github_issue_2657() {
1217        // Create a margin account
1218        let account_state = margin_account_state();
1219        let account = MarginAccount::new(account_state, false);
1220
1221        // Create BTCUSDT instrument
1222        let btcusdt = currency_pair_btcusdt();
1223        let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
1224
1225        // Create initial position with BUY 0.001 BTC at 50000.00
1226        let fill1 = OrderFilledSpec::builder()
1227            .instrument_id(btcusdt_any.id())
1228            .client_order_id(ClientOrderId::from("O-1"))
1229            .venue_order_id(VenueOrderId::from("V-1"))
1230            .trade_id(TradeId::from("T-1"))
1231            .last_qty(Quantity::from("0.001"))
1232            .last_px(Price::from("50000.00"))
1233            .currency(btcusdt_any.quote_currency())
1234            .ts_event(UnixNanos::from(1_000_000_000))
1235            .position_id(PositionId::from("P-GITHUB-2657"))
1236            .build();
1237
1238        let position = Position::new(&btcusdt_any, fill1);
1239
1240        // Create second fill that sells MORE than position size (0.002 > 0.001)
1241        let fill2 = OrderFilledSpec::builder()
1242            .instrument_id(btcusdt_any.id())
1243            .client_order_id(ClientOrderId::from("O-2"))
1244            .venue_order_id(VenueOrderId::from("V-2"))
1245            .trade_id(TradeId::from("T-2"))
1246            .order_side(OrderSide::Sell)
1247            .last_qty(Quantity::from("0.002")) // This is larger than position quantity!
1248            .last_px(Price::from("50075.00"))
1249            .currency(btcusdt_any.quote_currency())
1250            .ts_event(UnixNanos::from(2_000_000_000))
1251            .position_id(PositionId::from("P-GITHUB-2657"))
1252            .build();
1253
1254        // Test the fix - should only calculate PnL for position quantity (0.001), not fill quantity (0.002)
1255        let pnls = account
1256            .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1257            .unwrap();
1258
1259        // Should have exactly one PnL entry
1260        assert_eq!(pnls.len(), 1);
1261
1262        // Expected PnL should be for 0.001 BTC, not 0.002 BTC
1263        // PnL = (50075.00 - 50000.00) * 0.001 = 75.0 * 0.001 = 0.075 USDT
1264        let expected_pnl = Money::from("0.075 USDT");
1265        assert_eq!(pnls[0], expected_pnl);
1266    }
1267
1268    #[rstest]
1269    #[should_panic(expected = "not positive")]
1270    fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
1271        margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
1272    }
1273
1274    #[rstest]
1275    #[should_panic(expected = "not positive")]
1276    fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
1277        margin_account.set_default_leverage(Decimal::ZERO);
1278    }
1279
1280    #[rstest]
1281    #[should_panic(expected = "not positive")]
1282    fn test_set_leverage_negative_panics(
1283        mut margin_account: MarginAccount,
1284        audusd_sim: CurrencyPair,
1285    ) {
1286        margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
1287    }
1288
1289    #[rstest]
1290    fn test_calculate_pnls_with_same_side_fill_returns_empty() {
1291        use nautilus_core::UnixNanos;
1292
1293        use crate::{
1294            events::order::spec::OrderFilledSpec,
1295            identifiers::{ClientOrderId, PositionId, TradeId, VenueOrderId},
1296            instruments::InstrumentAny,
1297            position::Position,
1298            types::{Price, Quantity},
1299        };
1300
1301        // Create a margin account
1302        let account_state = margin_account_state();
1303        let account = MarginAccount::new(account_state, false);
1304
1305        // Create BTCUSDT instrument
1306        let btcusdt = currency_pair_btcusdt();
1307        let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
1308
1309        // Create initial position with BUY 1.0 BTC at 50000.00
1310        let fill1 = OrderFilledSpec::builder()
1311            .instrument_id(btcusdt.id)
1312            .client_order_id(ClientOrderId::from("O-1"))
1313            .venue_order_id(VenueOrderId::from("V-1"))
1314            .trade_id(TradeId::from("T-1"))
1315            .last_qty(Quantity::from("1.0"))
1316            .last_px(Price::from("50000.00"))
1317            .currency(btcusdt.quote_currency)
1318            .ts_event(UnixNanos::from(1_000_000_000))
1319            .position_id(PositionId::from("P-123456"))
1320            .build();
1321
1322        let position = Position::new(&btcusdt_any, fill1);
1323
1324        // Create second fill that also BUYS (same side as position entry)
1325        let fill2 = OrderFilledSpec::builder()
1326            .instrument_id(btcusdt.id)
1327            .client_order_id(ClientOrderId::from("O-2"))
1328            .venue_order_id(VenueOrderId::from("V-2"))
1329            .trade_id(TradeId::from("T-2"))
1330            .last_qty(Quantity::from("0.5"))
1331            .last_px(Price::from("51000.00"))
1332            .currency(btcusdt.quote_currency)
1333            .ts_event(UnixNanos::from(2_000_000_000))
1334            .position_id(PositionId::from("P-123456"))
1335            .build();
1336
1337        // Test that no PnL is calculated for same-side fills
1338        let pnls = account
1339            .calculate_pnls(&btcusdt_any, &fill2, Some(position))
1340            .unwrap();
1341
1342        // Should return empty PnL list
1343        assert_eq!(pnls.len(), 0);
1344    }
1345
1346    #[rstest]
1347    fn test_margin_accessor(
1348        mut margin_account: MarginAccount,
1349        instrument_id_aud_usd_sim: InstrumentId,
1350    ) {
1351        let margin_balance = MarginBalance::new(
1352            Money::from("1000 USD"),
1353            Money::from("500 USD"),
1354            Some(instrument_id_aud_usd_sim),
1355        );
1356
1357        margin_account.update_margin(margin_balance);
1358
1359        let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
1360        assert!(retrieved.is_some());
1361        let retrieved = retrieved.unwrap();
1362        assert_eq!(retrieved.initial, Money::from("1000 USD"));
1363        assert_eq!(retrieved.maintenance, Money::from("500 USD"));
1364        assert_eq!(retrieved.instrument_id, Some(instrument_id_aud_usd_sim));
1365    }
1366
1367    #[rstest]
1368    fn test_clear_margin(
1369        mut margin_account: MarginAccount,
1370        instrument_id_aud_usd_sim: InstrumentId,
1371    ) {
1372        let margin_balance = MarginBalance::new(
1373            Money::from("1000 USD"),
1374            Money::from("500 USD"),
1375            Some(instrument_id_aud_usd_sim),
1376        );
1377
1378        margin_account.update_margin(margin_balance);
1379        assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
1380
1381        margin_account.clear_margin(instrument_id_aud_usd_sim);
1382        assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
1383    }
1384
1385    #[rstest]
1386    fn test_update_margin_routes_account_wide(mut margin_account: MarginAccount) {
1387        let usd = Currency::USD();
1388        let margin_balance =
1389            MarginBalance::new(Money::from("200 USD"), Money::from("100 USD"), None);
1390
1391        margin_account.update_margin(margin_balance);
1392
1393        assert_eq!(margin_account.account_margin(&usd), Some(margin_balance));
1394        assert_eq!(
1395            margin_account.account_initial_margin(&usd),
1396            Some(Money::from("200 USD"))
1397        );
1398        assert_eq!(
1399            margin_account.account_maintenance_margin(&usd),
1400            Some(Money::from("100 USD"))
1401        );
1402
1403        margin_account.clear_account_margin(usd);
1404        assert!(margin_account.account_margin(&usd).is_none());
1405    }
1406
1407    #[rstest]
1408    fn test_total_margin_sums_per_instrument_and_account_wide(
1409        mut margin_account: MarginAccount,
1410        instrument_id_aud_usd_sim: InstrumentId,
1411    ) {
1412        let usd = Currency::USD();
1413        let baseline_initial = margin_account.total_initial_margin(usd);
1414        let baseline_maintenance = margin_account.total_maintenance_margin(usd);
1415
1416        margin_account.update_margin(MarginBalance::new(
1417            Money::from("100 USD"),
1418            Money::from("50 USD"),
1419            Some(instrument_id_aud_usd_sim),
1420        ));
1421        margin_account.update_margin(MarginBalance::new(
1422            Money::from("200 USD"),
1423            Money::from("150 USD"),
1424            None,
1425        ));
1426
1427        assert_eq!(
1428            margin_account.total_initial_margin(usd).raw,
1429            baseline_initial.raw + Money::from("300 USD").raw,
1430        );
1431        assert_eq!(
1432            margin_account.total_maintenance_margin(usd).raw,
1433            baseline_maintenance.raw + Money::from("200 USD").raw,
1434        );
1435    }
1436
1437    #[rstest]
1438    fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
1439        let option = option_contract_appl();
1440        let option_any = InstrumentAny::OptionContract(option.clone());
1441
1442        let order = OrderTestBuilder::new(OrderType::Market)
1443            .instrument_id(option.id)
1444            .side(OrderSide::Buy)
1445            .quantity(Quantity::from("10"))
1446            .build();
1447
1448        let fill = TestOrderEventStubs::filled(
1449            &order,
1450            &option_any,
1451            None,
1452            Some(PositionId::new("P-OPT-001")),
1453            Some(Price::from("5.50")),
1454            None,
1455            None,
1456            None,
1457            None,
1458            Some(AccountId::from("SIM-001")),
1459        );
1460
1461        let fill_owned: crate::events::OrderFilled = fill.into();
1462        let pnls = margin_account
1463            .calculate_pnls(&option_any, &fill_owned, None)
1464            .unwrap();
1465
1466        // BUY option = pay premium (negative PnL)
1467        // 10 contracts * $5.50 = $55.00 premium paid
1468        assert_eq!(pnls.len(), 1);
1469        assert_eq!(pnls[0], Money::from("-55 USD"));
1470    }
1471
1472    #[rstest]
1473    fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
1474        let option = option_contract_appl();
1475        let option_any = InstrumentAny::OptionContract(option.clone());
1476
1477        let order = OrderTestBuilder::new(OrderType::Market)
1478            .instrument_id(option.id)
1479            .side(OrderSide::Sell)
1480            .quantity(Quantity::from("10"))
1481            .build();
1482
1483        let fill = TestOrderEventStubs::filled(
1484            &order,
1485            &option_any,
1486            None,
1487            Some(PositionId::new("P-OPT-002")),
1488            Some(Price::from("5.50")),
1489            None,
1490            None,
1491            None,
1492            None,
1493            Some(AccountId::from("SIM-001")),
1494        );
1495
1496        let fill_owned: crate::events::OrderFilled = fill.into();
1497        let pnls = margin_account
1498            .calculate_pnls(&option_any, &fill_owned, None)
1499            .unwrap();
1500
1501        // SELL option = receive premium (positive PnL)
1502        // 10 contracts * $5.50 = $55.00 premium received
1503        assert_eq!(pnls.len(), 1);
1504        assert_eq!(pnls[0], Money::from("55 USD"));
1505    }
1506
1507    #[rstest]
1508    fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
1509        let binary = binary_option();
1510        let binary_any = InstrumentAny::BinaryOption(binary);
1511
1512        let order = OrderTestBuilder::new(OrderType::Market)
1513            .instrument_id(binary_any.id())
1514            .side(OrderSide::Buy)
1515            .quantity(Quantity::from("100"))
1516            .build();
1517
1518        let fill = TestOrderEventStubs::filled(
1519            &order,
1520            &binary_any,
1521            None,
1522            Some(PositionId::new("P-BIN-001")),
1523            Some(Price::from("0.65")),
1524            None,
1525            None,
1526            None,
1527            None,
1528            Some(AccountId::from("SIM-001")),
1529        );
1530
1531        let fill_owned: crate::events::OrderFilled = fill.into();
1532        let pnls = margin_account
1533            .calculate_pnls(&binary_any, &fill_owned, None)
1534            .unwrap();
1535
1536        assert_eq!(pnls.len(), 1);
1537        assert!(pnls[0].as_f64() < 0.0);
1538    }
1539}