use rust_decimal::Decimal;
use crate::{
instruments::Instrument,
types::{Money, Price, Quantity},
};
pub trait MarginModel {
fn calculate_initial_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money>;
fn calculate_maintenance_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money>;
}
#[derive(Debug, Clone)]
pub enum MarginModelAny {
Standard(StandardMarginModel),
Leveraged(LeveragedMarginModel),
}
impl MarginModel for MarginModelAny {
fn calculate_initial_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
match self {
Self::Standard(m) => m.calculate_initial_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
),
Self::Leveraged(m) => m.calculate_initial_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
),
}
}
fn calculate_maintenance_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
match self {
Self::Standard(m) => m.calculate_maintenance_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
),
Self::Leveraged(m) => m.calculate_maintenance_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
),
}
}
}
impl Default for MarginModelAny {
fn default() -> Self {
Self::Leveraged(LeveragedMarginModel)
}
}
fn margin_currency(
instrument: &dyn Instrument,
use_quote_for_inverse: bool,
) -> anyhow::Result<crate::types::Currency> {
if instrument.is_inverse() && !use_quote_for_inverse {
instrument.base_currency().ok_or_else(|| {
anyhow::anyhow!(
"Inverse instrument {} has no base currency",
instrument.id()
)
})
} else {
Ok(instrument.quote_currency())
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct StandardMarginModel;
impl MarginModel for StandardMarginModel {
fn calculate_initial_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
_leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
let use_quote = use_quote_for_inverse.unwrap_or(false);
let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
let margin = notional
.as_decimal()
.abs()
.checked_mul(instrument.margin_init())
.ok_or_else(|| anyhow::anyhow!("initial margin calculation overflow"))?;
let currency = margin_currency(instrument, use_quote)?;
Money::from_decimal(margin, currency).map_err(Into::into)
}
fn calculate_maintenance_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
_leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
let use_quote = use_quote_for_inverse.unwrap_or(false);
let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
let margin = notional
.as_decimal()
.abs()
.checked_mul(instrument.margin_maint())
.ok_or_else(|| anyhow::anyhow!("maintenance margin calculation overflow"))?;
let currency = margin_currency(instrument, use_quote)?;
Money::from_decimal(margin, currency).map_err(Into::into)
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct LeveragedMarginModel;
impl MarginModel for LeveragedMarginModel {
fn calculate_initial_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
if leverage <= Decimal::ZERO {
anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
}
let use_quote = use_quote_for_inverse.unwrap_or(false);
let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
let margin = notional
.as_decimal()
.abs()
.checked_div(leverage)
.and_then(|adjusted| adjusted.checked_mul(instrument.margin_init()))
.ok_or_else(|| anyhow::anyhow!("initial margin calculation overflow"))?;
let currency = margin_currency(instrument, use_quote)?;
Money::from_decimal(margin, currency).map_err(Into::into)
}
fn calculate_maintenance_margin(
&self,
instrument: &dyn Instrument,
quantity: Quantity,
price: Price,
leverage: Decimal,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
if leverage <= Decimal::ZERO {
anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
}
let use_quote = use_quote_for_inverse.unwrap_or(false);
let notional = instrument.try_calculate_notional_value(quantity, price, Some(use_quote))?;
let margin = notional
.as_decimal()
.abs()
.checked_div(leverage)
.and_then(|adjusted| adjusted.checked_mul(instrument.margin_maint()))
.ok_or_else(|| anyhow::anyhow!("maintenance margin calculation overflow"))?;
let currency = margin_currency(instrument, use_quote)?;
Money::from_decimal(margin, currency).map_err(Into::into)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use ustr::Ustr;
use super::*;
use crate::{
enums::AssetClass,
identifiers::{InstrumentId, Symbol},
instruments::{
CryptoPerpetual, FuturesSpread, Instrument, stubs::crypto_perpetual_ethusdt,
},
types::{Currency, Price, Quantity},
};
fn ethusdt() -> CryptoPerpetual {
crypto_perpetual_ethusdt()
}
#[rstest]
fn test_leveraged_initial_margin() {
let model = LeveragedMarginModel;
let instrument = ethusdt();
let quantity = Quantity::from("10.000");
let price = Price::from("5000.00");
let leverage = dec!(10);
let margin = model
.calculate_initial_margin(&instrument, quantity, price, leverage, None)
.unwrap();
let expected = Decimal::from(50000) / leverage * instrument.margin_init();
assert_eq!(margin.as_decimal(), expected);
assert_eq!(margin.currency, Currency::USDT());
}
#[rstest]
fn test_standard_ignores_leverage() {
let model = StandardMarginModel;
let instrument = ethusdt();
let quantity = Quantity::from("10.000");
let price = Price::from("5000.00");
let margin_low = model
.calculate_initial_margin(&instrument, quantity, price, dec!(2), None)
.unwrap();
let margin_high = model
.calculate_initial_margin(&instrument, quantity, price, dec!(100), None)
.unwrap();
assert_eq!(margin_low, margin_high);
}
fn negative_price_spread() -> FuturesSpread {
FuturesSpread::builder()
.instrument_id(InstrumentId::from("ESM4-ESU4.GLBX"))
.raw_symbol(Symbol::from("ESM4-ESU4"))
.asset_class(AssetClass::Index)
.underlying(Ustr::from("ES"))
.strategy_type(Ustr::from("EQ"))
.activation_ns(1_000.into())
.expiration_ns(2_000.into())
.currency(Currency::USD())
.price_precision(2)
.price_increment(Price::from("0.01"))
.multiplier(Quantity::from(50))
.lot_size(Quantity::from(1))
.margin_init(dec!(0.01))
.margin_maint(dec!(0.02))
.ts_event(1.into())
.ts_init(2.into())
.build()
.unwrap()
}
#[rstest]
fn test_standard_margin_is_positive_for_a_negative_price() {
let model = StandardMarginModel;
let instrument = negative_price_spread();
let quantity = Quantity::from(2);
let positive = Price::from("2.00");
let negative = Price::from("-2.00");
let initial = model
.calculate_initial_margin(&instrument, quantity, negative, dec!(1), None)
.unwrap();
let maintenance = model
.calculate_maintenance_margin(&instrument, quantity, negative, dec!(1), None)
.unwrap();
assert_eq!(initial.as_decimal(), dec!(2));
assert_eq!(maintenance.as_decimal(), dec!(4));
assert_eq!(
initial,
model
.calculate_initial_margin(&instrument, quantity, positive, dec!(1), None)
.unwrap()
);
}
#[rstest]
fn test_leveraged_margin_is_positive_for_a_negative_price() {
let model = LeveragedMarginModel;
let instrument = negative_price_spread();
let quantity = Quantity::from(2);
let negative = Price::from("-2.00");
let leverage = dec!(10);
let initial = model
.calculate_initial_margin(&instrument, quantity, negative, leverage, None)
.unwrap();
let maintenance = model
.calculate_maintenance_margin(&instrument, quantity, negative, leverage, None)
.unwrap();
assert_eq!(initial.as_decimal(), dec!(0.2));
assert_eq!(maintenance.as_decimal(), dec!(0.4));
}
#[rstest]
fn test_leveraged_zero_leverage_errors() {
let model = LeveragedMarginModel;
let instrument = ethusdt();
let result = model.calculate_initial_margin(
&instrument,
Quantity::from("1.000"),
Price::from("5000.00"),
Decimal::ZERO,
None,
);
assert!(result.is_err());
}
#[rstest]
fn test_leveraged_margin_decimal_overflow_returns_error() {
let model = LeveragedMarginModel;
let instrument = ethusdt();
let result = model.calculate_initial_margin(
&instrument,
Quantity::from("1.000"),
Price::from("5000.00"),
Decimal::new(1, 28),
None,
);
assert_eq!(
result.unwrap_err().to_string(),
"initial margin calculation overflow"
);
}
#[rstest]
fn test_margin_model_any_default_is_leveraged() {
let model = MarginModelAny::default();
assert!(matches!(model, MarginModelAny::Leveraged(_)));
}
#[rstest]
fn test_maintenance_margin() {
let model = LeveragedMarginModel;
let instrument = ethusdt();
let quantity = Quantity::from("10.000");
let price = Price::from("5000.00");
let leverage = dec!(10);
let margin = model
.calculate_maintenance_margin(&instrument, quantity, price, leverage, None)
.unwrap();
let expected = Decimal::from(50000) / leverage * instrument.margin_maint();
assert_eq!(margin.as_decimal(), expected);
}
}