1use rust_decimal::Decimal;
19
20use crate::{
21 instruments::Instrument,
22 types::{Money, Price, Quantity},
23};
24
25pub trait MarginModel {
27 fn calculate_initial_margin(
33 &self,
34 instrument: &dyn Instrument,
35 quantity: Quantity,
36 price: Price,
37 leverage: Decimal,
38 use_quote_for_inverse: Option<bool>,
39 ) -> anyhow::Result<Money>;
40
41 fn calculate_maintenance_margin(
47 &self,
48 instrument: &dyn Instrument,
49 quantity: Quantity,
50 price: Price,
51 leverage: Decimal,
52 use_quote_for_inverse: Option<bool>,
53 ) -> anyhow::Result<Money>;
54}
55
56#[derive(Debug, Clone)]
58pub enum MarginModelAny {
59 Standard(StandardMarginModel),
60 Leveraged(LeveragedMarginModel),
61}
62
63impl MarginModel for MarginModelAny {
64 fn calculate_initial_margin(
65 &self,
66 instrument: &dyn Instrument,
67 quantity: Quantity,
68 price: Price,
69 leverage: Decimal,
70 use_quote_for_inverse: Option<bool>,
71 ) -> anyhow::Result<Money> {
72 match self {
73 Self::Standard(m) => m.calculate_initial_margin(
74 instrument,
75 quantity,
76 price,
77 leverage,
78 use_quote_for_inverse,
79 ),
80 Self::Leveraged(m) => m.calculate_initial_margin(
81 instrument,
82 quantity,
83 price,
84 leverage,
85 use_quote_for_inverse,
86 ),
87 }
88 }
89
90 fn calculate_maintenance_margin(
91 &self,
92 instrument: &dyn Instrument,
93 quantity: Quantity,
94 price: Price,
95 leverage: Decimal,
96 use_quote_for_inverse: Option<bool>,
97 ) -> anyhow::Result<Money> {
98 match self {
99 Self::Standard(m) => m.calculate_maintenance_margin(
100 instrument,
101 quantity,
102 price,
103 leverage,
104 use_quote_for_inverse,
105 ),
106 Self::Leveraged(m) => m.calculate_maintenance_margin(
107 instrument,
108 quantity,
109 price,
110 leverage,
111 use_quote_for_inverse,
112 ),
113 }
114 }
115}
116
117impl Default for MarginModelAny {
118 fn default() -> Self {
119 Self::Leveraged(LeveragedMarginModel)
120 }
121}
122
123fn margin_currency(
125 instrument: &dyn Instrument,
126 use_quote_for_inverse: bool,
127) -> anyhow::Result<crate::types::Currency> {
128 if instrument.is_inverse() && !use_quote_for_inverse {
129 instrument.base_currency().ok_or_else(|| {
130 anyhow::anyhow!(
131 "Inverse instrument {} has no base currency",
132 instrument.id()
133 )
134 })
135 } else {
136 Ok(instrument.quote_currency())
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
146#[cfg_attr(
147 feature = "python",
148 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
149)]
150#[cfg_attr(
151 feature = "python",
152 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
153)]
154pub struct StandardMarginModel;
155
156impl MarginModel for StandardMarginModel {
157 fn calculate_initial_margin(
158 &self,
159 instrument: &dyn Instrument,
160 quantity: Quantity,
161 price: Price,
162 _leverage: Decimal,
163 use_quote_for_inverse: Option<bool>,
164 ) -> anyhow::Result<Money> {
165 let use_quote = use_quote_for_inverse.unwrap_or(false);
166 let notional = instrument.calculate_notional_value(quantity, price, Some(use_quote));
167 let margin = notional.as_decimal() * instrument.margin_init();
168 let currency = margin_currency(instrument, use_quote)?;
169 Money::from_decimal(margin, currency).map_err(Into::into)
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 let use_quote = use_quote_for_inverse.unwrap_or(false);
181 let notional = instrument.calculate_notional_value(quantity, price, Some(use_quote));
182 let margin = notional.as_decimal() * instrument.margin_maint();
183 let currency = margin_currency(instrument, use_quote)?;
184 Money::from_decimal(margin, currency).map_err(Into::into)
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
194#[cfg_attr(
195 feature = "python",
196 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
197)]
198#[cfg_attr(
199 feature = "python",
200 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
201)]
202pub struct LeveragedMarginModel;
203
204impl MarginModel for LeveragedMarginModel {
205 fn calculate_initial_margin(
206 &self,
207 instrument: &dyn Instrument,
208 quantity: Quantity,
209 price: Price,
210 leverage: Decimal,
211 use_quote_for_inverse: Option<bool>,
212 ) -> anyhow::Result<Money> {
213 if leverage <= Decimal::ZERO {
214 anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
215 }
216 let use_quote = use_quote_for_inverse.unwrap_or(false);
217 let notional = instrument.calculate_notional_value(quantity, price, Some(use_quote));
218 let adjusted = notional.as_decimal() / leverage;
219 let margin = adjusted * instrument.margin_init();
220 let currency = margin_currency(instrument, use_quote)?;
221 Money::from_decimal(margin, currency).map_err(Into::into)
222 }
223
224 fn calculate_maintenance_margin(
225 &self,
226 instrument: &dyn Instrument,
227 quantity: Quantity,
228 price: Price,
229 leverage: Decimal,
230 use_quote_for_inverse: Option<bool>,
231 ) -> anyhow::Result<Money> {
232 if leverage <= Decimal::ZERO {
233 anyhow::bail!("Invalid leverage {leverage} for {}", instrument.id());
234 }
235 let use_quote = use_quote_for_inverse.unwrap_or(false);
236 let notional = instrument.calculate_notional_value(quantity, price, Some(use_quote));
237 let adjusted = notional.as_decimal() / leverage;
238 let margin = adjusted * instrument.margin_maint();
239 let currency = margin_currency(instrument, use_quote)?;
240 Money::from_decimal(margin, currency).map_err(Into::into)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use rstest::rstest;
247 use rust_decimal::Decimal;
248 use rust_decimal_macros::dec;
249
250 use super::*;
251 use crate::{
252 instruments::{CryptoPerpetual, Instrument, stubs::crypto_perpetual_ethusdt},
253 types::{Currency, Price, Quantity},
254 };
255
256 fn ethusdt() -> CryptoPerpetual {
257 crypto_perpetual_ethusdt()
258 }
259
260 #[rstest]
261 fn test_leveraged_initial_margin() {
262 let model = LeveragedMarginModel;
263 let instrument = ethusdt();
264 let quantity = Quantity::from("10.000");
265 let price = Price::from("5000.00");
266 let leverage = dec!(10);
267
268 let margin = model
269 .calculate_initial_margin(&instrument, quantity, price, leverage, None)
270 .unwrap();
271
272 let expected = Decimal::from(50000) / leverage * instrument.margin_init();
275 assert_eq!(margin.as_decimal(), expected);
276 assert_eq!(margin.currency, Currency::USDT());
277 }
278
279 #[rstest]
280 fn test_standard_ignores_leverage() {
281 let model = StandardMarginModel;
282 let instrument = ethusdt();
283 let quantity = Quantity::from("10.000");
284 let price = Price::from("5000.00");
285
286 let margin_low = model
287 .calculate_initial_margin(&instrument, quantity, price, dec!(2), None)
288 .unwrap();
289 let margin_high = model
290 .calculate_initial_margin(&instrument, quantity, price, dec!(100), None)
291 .unwrap();
292
293 assert_eq!(margin_low, margin_high);
295 }
296
297 #[rstest]
298 fn test_leveraged_zero_leverage_errors() {
299 let model = LeveragedMarginModel;
300 let instrument = ethusdt();
301
302 let result = model.calculate_initial_margin(
303 &instrument,
304 Quantity::from("1.000"),
305 Price::from("5000.00"),
306 Decimal::ZERO,
307 None,
308 );
309
310 assert!(result.is_err());
311 }
312
313 #[rstest]
314 fn test_margin_model_any_default_is_leveraged() {
315 let model = MarginModelAny::default();
316 assert!(matches!(model, MarginModelAny::Leveraged(_)));
317 }
318
319 #[rstest]
320 fn test_maintenance_margin() {
321 let model = LeveragedMarginModel;
322 let instrument = ethusdt();
323 let quantity = Quantity::from("10.000");
324 let price = Price::from("5000.00");
325 let leverage = dec!(10);
326
327 let margin = model
328 .calculate_maintenance_margin(&instrument, quantity, price, leverage, None)
329 .unwrap();
330
331 let expected = Decimal::from(50000) / leverage * instrument.margin_maint();
332 assert_eq!(margin.as_decimal(), expected);
333 }
334}