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