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}
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#[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 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#[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 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 assert_eq!(margin_low, margin_high);
433 }
434
435 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 assert_eq!(initial.as_decimal(), dec!(2));
477 assert_eq!(maintenance.as_decimal(), dec!(4));
478 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 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 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 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 #[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}