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 rust_decimal::Decimal;
19
20use crate::{
21    instruments::Instrument,
22    types::{Money, Price, Quantity},
23};
24
25/// Determines how margin requirements are calculated for leveraged positions.
26pub trait MarginModel {
27    /// Calculates the initial (order) margin requirement.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if margin cannot be computed (e.g. invalid instrument).
32    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    /// Calculates the maintenance (position) margin requirement.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if margin cannot be computed (e.g. invalid instrument).
46    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/// Enum dispatch for [`MarginModel`] implementations.
57#[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
123/// Resolves the margin currency based on instrument properties.
124fn 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/// Uses fixed margin percentages without leverage division.
141///
142/// Margin is calculated as `notional_value * margin_rate`, ignoring the
143/// account leverage. Appropriate for traditional brokers where margin
144/// requirements are fixed percentages of notional value.
145#[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/// Divides notional value by leverage before applying margin rates.
189///
190/// Margin is calculated as `(notional_value / leverage) * margin_rate`.
191/// This is the default model, appropriate for crypto exchanges and venues
192/// where leverage directly reduces margin requirements.
193#[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        // notional = 10 * 5000 = 50000, adjusted = 50000/10 = 5000
273        // margin = 5000 * margin_init
274        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        // StandardMarginModel ignores leverage so both should be equal
294        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}