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