Skip to main content

nautilus_model/accounts/
margin_model.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//! Pluggable margin calculation models for [`MarginAccount`](super::MarginAccount).
17
18use std::{fmt::Debug, sync::Arc};
19
20use rust_decimal::Decimal;
21
22use crate::{
23    instruments::Instrument,
24    types::{Money, Price, Quantity},
25};
26
27/// Determines how margin requirements are calculated for leveraged positions.
28pub trait MarginModel: Send + Sync {
29    /// Returns the stable model name used in canonical backtest results.
30    #[must_use]
31    fn name(&self) -> &'static str;
32
33    /// Calculates the initial (order) margin requirement.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if margin cannot be computed (e.g. invalid instrument).
38    fn calculate_initial_margin(
39        &self,
40        instrument: &dyn Instrument,
41        quantity: Quantity,
42        price: Price,
43        leverage: Decimal,
44        use_quote_for_inverse: Option<bool>,
45    ) -> anyhow::Result<Money>;
46
47    /// Calculates the maintenance (position) margin requirement.
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if margin cannot be computed (e.g. invalid instrument).
52    fn calculate_maintenance_margin(
53        &self,
54        instrument: &dyn Instrument,
55        quantity: Quantity,
56        price: Price,
57        leverage: Decimal,
58        use_quote_for_inverse: Option<bool>,
59    ) -> anyhow::Result<Money>;
60}
61
62/// Shared runtime handle for a margin model.
63#[derive(Clone)]
64pub struct MarginModelHandle(Arc<dyn MarginModel>);
65
66impl MarginModelHandle {
67    /// Creates a new [`MarginModelHandle`] from a margin model.
68    #[must_use]
69    pub fn new<T>(model: T) -> Self
70    where
71        T: MarginModel + 'static,
72    {
73        Self(Arc::new(model))
74    }
75
76    /// Creates a new [`MarginModelHandle`] from an existing atomically reference-counted model.
77    #[must_use]
78    pub fn from_arc(model: Arc<dyn MarginModel>) -> Self {
79        Self(model)
80    }
81}
82
83impl Debug for MarginModelHandle {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_tuple(stringify!(MarginModelHandle))
86            .field(&"<dyn MarginModel>")
87            .finish()
88    }
89}
90
91impl MarginModel for MarginModelHandle {
92    fn name(&self) -> &'static str {
93        self.0.name()
94    }
95
96    fn calculate_initial_margin(
97        &self,
98        instrument: &dyn Instrument,
99        quantity: Quantity,
100        price: Price,
101        leverage: Decimal,
102        use_quote_for_inverse: Option<bool>,
103    ) -> anyhow::Result<Money> {
104        self.0.calculate_initial_margin(
105            instrument,
106            quantity,
107            price,
108            leverage,
109            use_quote_for_inverse,
110        )
111    }
112
113    fn calculate_maintenance_margin(
114        &self,
115        instrument: &dyn Instrument,
116        quantity: Quantity,
117        price: Price,
118        leverage: Decimal,
119        use_quote_for_inverse: Option<bool>,
120    ) -> anyhow::Result<Money> {
121        self.0.calculate_maintenance_margin(
122            instrument,
123            quantity,
124            price,
125            leverage,
126            use_quote_for_inverse,
127        )
128    }
129}
130
131/// Enum dispatch for [`MarginModel`] implementations.
132#[derive(Debug, Clone)]
133pub enum MarginModelAny {
134    Standard(StandardMarginModel),
135    Leveraged(LeveragedMarginModel),
136    #[cfg(feature = "python")]
137    Python(crate::python::account::margin_model::PythonMarginModel),
138}
139
140impl MarginModel for MarginModelAny {
141    fn name(&self) -> &'static str {
142        match self {
143            Self::Standard(model) => model.name(),
144            Self::Leveraged(model) => model.name(),
145            #[cfg(feature = "python")]
146            Self::Python(model) => model.name(),
147        }
148    }
149
150    fn calculate_initial_margin(
151        &self,
152        instrument: &dyn Instrument,
153        quantity: Quantity,
154        price: Price,
155        leverage: Decimal,
156        use_quote_for_inverse: Option<bool>,
157    ) -> anyhow::Result<Money> {
158        match self {
159            Self::Standard(m) => m.calculate_initial_margin(
160                instrument,
161                quantity,
162                price,
163                leverage,
164                use_quote_for_inverse,
165            ),
166            Self::Leveraged(m) => m.calculate_initial_margin(
167                instrument,
168                quantity,
169                price,
170                leverage,
171                use_quote_for_inverse,
172            ),
173            #[cfg(feature = "python")]
174            Self::Python(m) => m.calculate_initial_margin(
175                instrument,
176                quantity,
177                price,
178                leverage,
179                use_quote_for_inverse,
180            ),
181        }
182    }
183
184    fn calculate_maintenance_margin(
185        &self,
186        instrument: &dyn Instrument,
187        quantity: Quantity,
188        price: Price,
189        leverage: Decimal,
190        use_quote_for_inverse: Option<bool>,
191    ) -> anyhow::Result<Money> {
192        match self {
193            Self::Standard(m) => m.calculate_maintenance_margin(
194                instrument,
195                quantity,
196                price,
197                leverage,
198                use_quote_for_inverse,
199            ),
200            Self::Leveraged(m) => m.calculate_maintenance_margin(
201                instrument,
202                quantity,
203                price,
204                leverage,
205                use_quote_for_inverse,
206            ),
207            #[cfg(feature = "python")]
208            Self::Python(m) => m.calculate_maintenance_margin(
209                instrument,
210                quantity,
211                price,
212                leverage,
213                use_quote_for_inverse,
214            ),
215        }
216    }
217}
218
219impl Default for MarginModelAny {
220    fn default() -> Self {
221        Self::Leveraged(LeveragedMarginModel)
222    }
223}
224
225impl Default for MarginModelHandle {
226    fn default() -> Self {
227        MarginModelAny::default().into()
228    }
229}
230
231impl From<MarginModelAny> for MarginModelHandle {
232    fn from(model: MarginModelAny) -> Self {
233        Self::new(model)
234    }
235}
236
237/// Uses fixed margin percentages without leverage division.
238///
239/// Margin is calculated as `notional_value * margin_rate`, ignoring the
240/// account leverage. Appropriate for traditional brokers where margin
241/// requirements are fixed percentages of notional value.
242#[derive(Debug, Clone, Copy)]
243#[cfg_attr(
244    feature = "python",
245    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
246)]
247#[cfg_attr(
248    feature = "python",
249    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
250)]
251pub struct StandardMarginModel;
252
253impl MarginModel for StandardMarginModel {
254    fn name(&self) -> &'static str {
255        "standard"
256    }
257
258    fn calculate_initial_margin(
259        &self,
260        instrument: &dyn Instrument,
261        quantity: Quantity,
262        price: Price,
263        _leverage: Decimal,
264        use_quote_for_inverse: Option<bool>,
265    ) -> anyhow::Result<Money> {
266        let use_quote = use_quote_for_inverse.unwrap_or(false);
267        let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
268        // Spreads and options may quote negative, which carries the sign into the notional.
269        // A requirement is a reserve against exposure magnitude, so take it on `abs`.
270        let margin = notional
271            .as_decimal()
272            .abs()
273            .checked_mul(instrument.margin_init())
274            .ok_or_else(|| anyhow::anyhow!("initial margin calculation overflow"))?;
275        Money::from_decimal(margin, notional.currency).map_err(Into::into)
276    }
277
278    fn calculate_maintenance_margin(
279        &self,
280        instrument: &dyn Instrument,
281        quantity: Quantity,
282        price: Price,
283        _leverage: Decimal,
284        use_quote_for_inverse: Option<bool>,
285    ) -> anyhow::Result<Money> {
286        let use_quote = use_quote_for_inverse.unwrap_or(false);
287        let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
288        let margin = notional
289            .as_decimal()
290            .abs()
291            .checked_mul(instrument.margin_maint())
292            .ok_or_else(|| anyhow::anyhow!("maintenance margin calculation overflow"))?;
293        Money::from_decimal(margin, notional.currency).map_err(Into::into)
294    }
295}
296
297/// Divides notional value by leverage before applying margin rates.
298///
299/// Margin is calculated as `(notional_value / leverage) * margin_rate`.
300/// This is the default model, appropriate for crypto exchanges and venues
301/// where leverage directly reduces margin requirements.
302#[derive(Debug, Clone, Copy)]
303#[cfg_attr(
304    feature = "python",
305    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
306)]
307#[cfg_attr(
308    feature = "python",
309    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
310)]
311pub struct LeveragedMarginModel;
312
313impl MarginModel for LeveragedMarginModel {
314    fn name(&self) -> &'static str {
315        "leveraged"
316    }
317
318    fn calculate_initial_margin(
319        &self,
320        instrument: &dyn Instrument,
321        quantity: Quantity,
322        price: Price,
323        leverage: Decimal,
324        use_quote_for_inverse: Option<bool>,
325    ) -> anyhow::Result<Money> {
326        if leverage <= Decimal::ZERO {
327            anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
328        }
329        let use_quote = use_quote_for_inverse.unwrap_or(false);
330        let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
331        let margin = notional
332            .as_decimal()
333            .abs()
334            .checked_div(leverage)
335            .and_then(|adjusted| adjusted.checked_mul(instrument.margin_init()))
336            .ok_or_else(|| anyhow::anyhow!("initial margin calculation overflow"))?;
337        Money::from_decimal(margin, notional.currency).map_err(Into::into)
338    }
339
340    fn calculate_maintenance_margin(
341        &self,
342        instrument: &dyn Instrument,
343        quantity: Quantity,
344        price: Price,
345        leverage: Decimal,
346        use_quote_for_inverse: Option<bool>,
347    ) -> anyhow::Result<Money> {
348        if leverage <= Decimal::ZERO {
349            anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
350        }
351        let use_quote = use_quote_for_inverse.unwrap_or(false);
352        let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
353        let margin = notional
354            .as_decimal()
355            .abs()
356            .checked_div(leverage)
357            .and_then(|adjusted| adjusted.checked_mul(instrument.margin_maint()))
358            .ok_or_else(|| anyhow::anyhow!("maintenance margin calculation overflow"))?;
359        Money::from_decimal(margin, notional.currency).map_err(Into::into)
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use rstest::rstest;
366    use rust_decimal::Decimal;
367    use rust_decimal_macros::dec;
368    use ustr::Ustr;
369
370    use super::*;
371    use crate::{
372        enums::AssetClass,
373        identifiers::{InstrumentId, Symbol},
374        instruments::{
375            CryptoFuture, CryptoPerpetual, FuturesSpread, Instrument,
376            stubs::{btcusd_inverse_perp, crypto_perpetual_ethusdt, ethbtc_quanto},
377        },
378        types::{Currency, Price, Quantity},
379    };
380
381    struct FixedMarginModel {
382        initial: Money,
383        maintenance: Money,
384    }
385
386    impl MarginModel for FixedMarginModel {
387        fn name(&self) -> &'static str {
388            "fixed"
389        }
390
391        fn calculate_initial_margin(
392            &self,
393            _instrument: &dyn Instrument,
394            _quantity: Quantity,
395            _price: Price,
396            _leverage: Decimal,
397            _use_quote_for_inverse: Option<bool>,
398        ) -> anyhow::Result<Money> {
399            Ok(self.initial)
400        }
401
402        fn calculate_maintenance_margin(
403            &self,
404            _instrument: &dyn Instrument,
405            _quantity: Quantity,
406            _price: Price,
407            _leverage: Decimal,
408            _use_quote_for_inverse: Option<bool>,
409        ) -> anyhow::Result<Money> {
410            Ok(self.maintenance)
411        }
412    }
413
414    fn ethusdt() -> CryptoPerpetual {
415        crypto_perpetual_ethusdt()
416    }
417
418    #[rstest]
419    fn test_leveraged_initial_margin() {
420        let model = LeveragedMarginModel;
421        let instrument = ethusdt();
422        let quantity = Quantity::from("10.000");
423        let price = Price::from("5000.00");
424        let leverage = dec!(10);
425
426        let margin = model
427            .calculate_initial_margin(&instrument, quantity, price, leverage, None)
428            .unwrap();
429
430        // notional = 10 * 5000 = 50000, adjusted = 50000/10 = 5000
431        // margin = 5000 * margin_init
432        let expected = Decimal::from(50000) / leverage * instrument.margin_init();
433        assert_eq!(margin.as_decimal(), expected);
434        assert_eq!(margin.currency, Currency::USDT());
435    }
436
437    #[rstest]
438    fn test_standard_ignores_leverage() {
439        let model = StandardMarginModel;
440        let instrument = ethusdt();
441        let quantity = Quantity::from("10.000");
442        let price = Price::from("5000.00");
443
444        let margin_low = model
445            .calculate_initial_margin(&instrument, quantity, price, dec!(2), None)
446            .unwrap();
447        let margin_high = model
448            .calculate_initial_margin(&instrument, quantity, price, dec!(100), None)
449            .unwrap();
450
451        // StandardMarginModel ignores leverage so both should be equal
452        assert_eq!(margin_low, margin_high);
453    }
454
455    /// A spread carrying non-zero margin rates, so the assertions below cannot pass on a
456    /// zero requirement. `FuturesSpread` is one of the classes permitting a negative
457    /// price (see `InstrumentClass::allows_negative_price`).
458    fn negative_price_spread() -> FuturesSpread {
459        FuturesSpread::builder()
460            .instrument_id(InstrumentId::from("ESM4-ESU4.GLBX"))
461            .raw_symbol(Symbol::from("ESM4-ESU4"))
462            .asset_class(AssetClass::Index)
463            .underlying(Ustr::from("ES"))
464            .strategy_type(Ustr::from("EQ"))
465            .activation_ns(1_000.into())
466            .expiration_ns(2_000.into())
467            .currency(Currency::USD())
468            .price_precision(2)
469            .price_increment(Price::from("0.01"))
470            .multiplier(Quantity::from(50))
471            .lot_size(Quantity::from(1))
472            .margin_init(dec!(0.01))
473            .margin_maint(dec!(0.02))
474            .ts_event(1.into())
475            .ts_init(2.into())
476            .build()
477            .unwrap()
478    }
479
480    #[rstest]
481    fn test_standard_margin_is_positive_for_a_negative_price() {
482        let model = StandardMarginModel;
483        let instrument = negative_price_spread();
484        let quantity = Quantity::from(2);
485        let positive = Price::from("2.00");
486        let negative = Price::from("-2.00");
487
488        let initial = model
489            .calculate_initial_margin(&instrument, quantity, negative, dec!(1), None)
490            .unwrap();
491        let maintenance = model
492            .calculate_maintenance_margin(&instrument, quantity, negative, dec!(1), None)
493            .unwrap();
494
495        // notional magnitude = 2 * 50 * 2.00 = 200
496        assert_eq!(initial.as_decimal(), dec!(2));
497        assert_eq!(maintenance.as_decimal(), dec!(4));
498        // A negative quote reserves the same as the equivalent positive one.
499        assert_eq!(
500            initial,
501            model
502                .calculate_initial_margin(&instrument, quantity, positive, dec!(1), None)
503                .unwrap()
504        );
505    }
506
507    #[rstest]
508    fn test_leveraged_margin_is_positive_for_a_negative_price() {
509        let model = LeveragedMarginModel;
510        let instrument = negative_price_spread();
511        let quantity = Quantity::from(2);
512        let negative = Price::from("-2.00");
513        let leverage = dec!(10);
514
515        let initial = model
516            .calculate_initial_margin(&instrument, quantity, negative, leverage, None)
517            .unwrap();
518        let maintenance = model
519            .calculate_maintenance_margin(&instrument, quantity, negative, leverage, None)
520            .unwrap();
521
522        // notional magnitude = 200, adjusted = 200 / 10 = 20
523        assert_eq!(initial.as_decimal(), dec!(0.2));
524        assert_eq!(maintenance.as_decimal(), dec!(0.4));
525    }
526
527    #[rstest]
528    #[case::zero(Decimal::ZERO)]
529    #[case::negative(dec!(-1))]
530    fn test_leveraged_rejects_non_positive_leverage(#[case] leverage: Decimal) {
531        let model = LeveragedMarginModel;
532        let instrument = ethusdt();
533        let quantity = Quantity::from("1.000");
534        let price = Price::from("5000.00");
535        let expected = format!("Invalid leverage {leverage} for {}", instrument.id());
536
537        let initial = model.calculate_initial_margin(&instrument, quantity, price, leverage, None);
538        let maintenance =
539            model.calculate_maintenance_margin(&instrument, quantity, price, leverage, None);
540
541        assert_eq!(initial.unwrap_err().to_string(), expected);
542        assert_eq!(maintenance.unwrap_err().to_string(), expected);
543    }
544
545    #[rstest]
546    fn test_leveraged_margin_decimal_overflow_returns_error() {
547        let model = LeveragedMarginModel;
548        let instrument = ethusdt();
549        let quantity = Quantity::from("1.000");
550        let price = Price::from("5000.00");
551        let leverage = Decimal::new(1, 28);
552
553        let initial = model.calculate_initial_margin(&instrument, quantity, price, leverage, None);
554        let maintenance =
555            model.calculate_maintenance_margin(&instrument, quantity, price, leverage, None);
556
557        assert_eq!(
558            initial.unwrap_err().to_string(),
559            "initial margin calculation overflow"
560        );
561        assert_eq!(
562            maintenance.unwrap_err().to_string(),
563            "maintenance margin calculation overflow"
564        );
565    }
566
567    #[rstest]
568    fn test_margin_model_any_default_is_leveraged() {
569        let model = MarginModelAny::default();
570        let instrument = ethusdt();
571        let quantity = Quantity::from("10.000");
572        let price = Price::from("5000.00");
573        let leverage = dec!(10);
574
575        let initial = model
576            .calculate_initial_margin(&instrument, quantity, price, leverage, None)
577            .unwrap();
578        let maintenance = model
579            .calculate_maintenance_margin(&instrument, quantity, price, leverage, None)
580            .unwrap();
581
582        // notional = 10 * 5000 = 50000, adjusted = 50000 / 10 = 5000
583        assert!(matches!(model, MarginModelAny::Leveraged(_)));
584        assert_eq!(model.name(), "leveraged");
585        assert_eq!(
586            initial.as_decimal(),
587            Decimal::from(5000) * instrument.margin_init()
588        );
589        assert_eq!(
590            maintenance.as_decimal(),
591            Decimal::from(5000) * instrument.margin_maint()
592        );
593    }
594
595    #[rstest]
596    fn test_margin_model_any_standard_dispatches_to_the_standard_model() {
597        let model = MarginModelAny::Standard(StandardMarginModel);
598        let instrument = ethusdt();
599        let quantity = Quantity::from("10.000");
600        let price = Price::from("5000.00");
601        let leverage = dec!(10);
602
603        let initial = model
604            .calculate_initial_margin(&instrument, quantity, price, leverage, None)
605            .unwrap();
606        let maintenance = model
607            .calculate_maintenance_margin(&instrument, quantity, price, leverage, None)
608            .unwrap();
609
610        // notional = 10 * 5000 = 50000, which the standard model reserves against in full
611        assert_eq!(model.name(), "standard");
612        assert_eq!(
613            initial.as_decimal(),
614            Decimal::from(50000) * instrument.margin_init()
615        );
616        assert_eq!(
617            maintenance.as_decimal(),
618            Decimal::from(50000) * instrument.margin_maint()
619        );
620        assert_eq!(initial.currency, Currency::USDT());
621    }
622
623    #[rstest]
624    fn test_margin_model_handle_calls_custom_model() {
625        let initial = Money::from("12.34 USDT");
626        let maintenance = Money::from("5.67 USDT");
627        let model: Arc<dyn MarginModel> = Arc::new(FixedMarginModel {
628            initial,
629            maintenance,
630        });
631        let handle = MarginModelHandle::from_arc(model);
632        let cloned_handle = handle.clone();
633        drop(handle);
634        let instrument = ethusdt();
635
636        let initial_result = cloned_handle
637            .calculate_initial_margin(
638                &instrument,
639                Quantity::from("1.000"),
640                Price::from("5000.00"),
641                dec!(10),
642                None,
643            )
644            .unwrap();
645        let maintenance_result = cloned_handle
646            .calculate_maintenance_margin(
647                &instrument,
648                Quantity::from("1.000"),
649                Price::from("5000.00"),
650                dec!(10),
651                None,
652            )
653            .unwrap();
654
655        assert_eq!(cloned_handle.name(), "fixed");
656        assert_eq!(initial_result, initial);
657        assert_eq!(maintenance_result, maintenance);
658    }
659
660    #[rstest]
661    fn test_maintenance_margin() {
662        let model = LeveragedMarginModel;
663        let instrument = ethusdt();
664        let quantity = Quantity::from("10.000");
665        let price = Price::from("5000.00");
666        let leverage = dec!(10);
667
668        let margin = model
669            .calculate_maintenance_margin(&instrument, quantity, price, leverage, None)
670            .unwrap();
671
672        let expected = Decimal::from(50000) / leverage * instrument.margin_maint();
673        assert_eq!(margin.as_decimal(), expected);
674    }
675
676    // Rates are 0.1 initial and 0.05 maintenance, notionals are 50000 USDT (linear),
677    // 2 BTC (inverse) and 5 USDT (quanto), halved by the leveraged model at 2x leverage.
678    #[rstest]
679    #[case::standard_linear(
680        MarginModelAny::Standard(StandardMarginModel),
681        "linear",
682        "5000 USDT",
683        "2500 USDT"
684    )]
685    #[case::standard_inverse(
686        MarginModelAny::Standard(StandardMarginModel),
687        "inverse",
688        "0.2 BTC",
689        "0.1 BTC"
690    )]
691    #[case::standard_quanto(
692        MarginModelAny::Standard(StandardMarginModel),
693        "quanto",
694        "0.5 USDT",
695        "0.25 USDT"
696    )]
697    #[case::leveraged_linear(
698        MarginModelAny::Leveraged(LeveragedMarginModel),
699        "linear",
700        "2500 USDT",
701        "1250 USDT"
702    )]
703    #[case::leveraged_inverse(
704        MarginModelAny::Leveraged(LeveragedMarginModel),
705        "inverse",
706        "0.1 BTC",
707        "0.05 BTC"
708    )]
709    #[case::leveraged_quanto(
710        MarginModelAny::Leveraged(LeveragedMarginModel),
711        "quanto",
712        "0.25 USDT",
713        "0.125 USDT"
714    )]
715    fn test_margin_amount_and_currency(
716        #[case] model: MarginModelAny,
717        #[case] contract: &str,
718        #[case] expected_initial: &str,
719        #[case] expected_maintenance: &str,
720        mut crypto_perpetual_ethusdt: CryptoPerpetual,
721        mut btcusd_inverse_perp: CryptoPerpetual,
722        mut ethbtc_quanto: CryptoFuture,
723    ) {
724        let (instrument, quantity, price) = match contract {
725            "linear" => {
726                crypto_perpetual_ethusdt.margin_init = dec!(0.1);
727                crypto_perpetual_ethusdt.margin_maint = dec!(0.05);
728                let quantity = Quantity::from("10.000");
729                (
730                    crypto_perpetual_ethusdt.into_any(),
731                    quantity,
732                    Price::from("5000.00"),
733                )
734            }
735            "inverse" => {
736                btcusd_inverse_perp.margin_init = dec!(0.1);
737                btcusd_inverse_perp.margin_maint = dec!(0.05);
738                let quantity = Quantity::from("100000");
739                (
740                    btcusd_inverse_perp.into_any(),
741                    quantity,
742                    Price::from("50000.0"),
743                )
744            }
745            "quanto" => {
746                ethbtc_quanto.margin_init = dec!(0.1);
747                ethbtc_quanto.margin_maint = dec!(0.05);
748                let quantity = Quantity::from("100.000");
749                (ethbtc_quanto.into_any(), quantity, Price::from("0.05000"))
750            }
751            _ => unreachable!(),
752        };
753
754        let initial = model
755            .calculate_initial_margin(&instrument, quantity, price, dec!(2), None)
756            .unwrap();
757        let maintenance = model
758            .calculate_maintenance_margin(&instrument, quantity, price, dec!(2), None)
759            .unwrap();
760
761        assert_eq!(initial, Money::from(expected_initial));
762        assert_eq!(maintenance, Money::from(expected_maintenance));
763    }
764}