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