1use std::{fmt::Debug, sync::Arc};
19
20use rust_decimal::Decimal;
21
22use crate::{
23 instruments::Instrument,
24 types::{Money, Price, Quantity},
25};
26
27pub trait MarginModel: Send + Sync {
29 #[must_use]
31 fn name(&self) -> &'static str;
32
33 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 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#[derive(Clone)]
64pub struct MarginModelHandle(Arc<dyn MarginModel>);
65
66impl MarginModelHandle {
67 #[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 #[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#[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#[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 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#[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 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 assert_eq!(margin_low, margin_high);
453 }
454
455 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 assert_eq!(initial.as_decimal(), dec!(2));
497 assert_eq!(maintenance.as_decimal(), dec!(4));
498 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 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 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 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 #[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}