nautilus_model/instruments/
futures_contract.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19    correctness::{check_equal_u8, check_valid_string, check_valid_string_optional, FAILED},
20    UnixNanos,
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{any::InstrumentAny, Instrument};
27use crate::{
28    enums::{AssetClass, InstrumentClass, OptionKind},
29    identifiers::{InstrumentId, Symbol},
30    types::{
31        currency::Currency,
32        money::Money,
33        price::{check_positive_price, Price},
34        quantity::{check_positive_quantity, Quantity},
35    },
36};
37
38/// Represents a generic deliverable futures contract instrument.
39#[repr(C)]
40#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
44)]
45pub struct FuturesContract {
46    /// The instrument ID.
47    pub id: InstrumentId,
48    /// The raw/local/native symbol for the instrument, assigned by the venue.
49    pub raw_symbol: Symbol,
50    /// The futures contract asset class.
51    pub asset_class: AssetClass,
52    /// The exchange ISO 10383 Market Identifier Code (MIC) where the instrument trades.
53    pub exchange: Option<Ustr>,
54    /// The underlying asset.
55    pub underlying: Ustr,
56    /// UNIX timestamp (nanoseconds) for contract activation.
57    pub activation_ns: UnixNanos,
58    /// UNIX timestamp (nanoseconds) for contract expiration.
59    pub expiration_ns: UnixNanos,
60    /// The futures contract currency.
61    pub currency: Currency,
62    /// The price decimal precision.
63    pub price_precision: u8,
64    /// The minimum price increment (tick size).
65    pub price_increment: Price,
66    /// The minimum size increment.
67    pub size_increment: Quantity,
68    /// The trading size decimal precision.
69    pub size_precision: u8,
70    /// The contract multiplier.
71    pub multiplier: Quantity,
72    /// The rounded lot unit size (standard/board).
73    pub lot_size: Quantity,
74    /// The initial (order) margin requirement in percentage of order value.
75    pub margin_init: Decimal,
76    /// The maintenance (position) margin in percentage of position value.
77    pub margin_maint: Decimal,
78    /// The fee rate for liquidity makers as a percentage of order value.
79    pub maker_fee: Decimal,
80    /// The fee rate for liquidity takers as a percentage of order value.
81    pub taker_fee: Decimal,
82    /// The maximum allowable order quantity.
83    pub max_quantity: Option<Quantity>,
84    /// The minimum allowable order quantity.
85    pub min_quantity: Option<Quantity>,
86    /// The maximum allowable quoted price.
87    pub max_price: Option<Price>,
88    /// The minimum allowable quoted price.
89    pub min_price: Option<Price>,
90    /// UNIX timestamp (nanoseconds) when the data event occurred.
91    pub ts_event: UnixNanos,
92    /// UNIX timestamp (nanoseconds) when the data object was initialized.
93    pub ts_init: UnixNanos,
94}
95
96impl FuturesContract {
97    /// Creates a new [`FuturesContract`] instance with correctness checking.
98    ///
99    /// # Notes
100    ///
101    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
102    #[allow(clippy::too_many_arguments)]
103    pub fn new_checked(
104        id: InstrumentId,
105        raw_symbol: Symbol,
106        asset_class: AssetClass,
107        exchange: Option<Ustr>,
108        underlying: Ustr,
109        activation_ns: UnixNanos,
110        expiration_ns: UnixNanos,
111        currency: Currency,
112        price_precision: u8,
113        price_increment: Price,
114        multiplier: Quantity,
115        lot_size: Quantity,
116        max_quantity: Option<Quantity>,
117        min_quantity: Option<Quantity>,
118        max_price: Option<Price>,
119        min_price: Option<Price>,
120        margin_init: Option<Decimal>,
121        margin_maint: Option<Decimal>,
122        maker_fee: Option<Decimal>,
123        taker_fee: Option<Decimal>,
124        ts_event: UnixNanos,
125        ts_init: UnixNanos,
126    ) -> anyhow::Result<Self> {
127        check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?;
128        check_valid_string(underlying.as_str(), stringify!(underlying))?;
129        check_equal_u8(
130            price_precision,
131            price_increment.precision,
132            stringify!(price_precision),
133            stringify!(price_increment.precision),
134        )?;
135        check_positive_price(price_increment.raw, stringify!(price_increment.raw))?;
136        check_positive_quantity(multiplier.raw, stringify!(multiplier.raw))?;
137        check_positive_quantity(lot_size.raw, stringify!(lot_size.raw))?;
138
139        Ok(Self {
140            id,
141            raw_symbol,
142            asset_class,
143            exchange,
144            underlying,
145            activation_ns,
146            expiration_ns,
147            currency,
148            price_precision,
149            price_increment,
150            size_precision: 0,
151            size_increment: Quantity::from(1),
152            multiplier,
153            lot_size,
154            max_quantity,
155            min_quantity: Some(min_quantity.unwrap_or(1.into())),
156            max_price,
157            min_price,
158            margin_init: margin_init.unwrap_or_default(),
159            margin_maint: margin_maint.unwrap_or_default(),
160            maker_fee: maker_fee.unwrap_or_default(),
161            taker_fee: taker_fee.unwrap_or_default(),
162            ts_event,
163            ts_init,
164        })
165    }
166
167    /// Creates a new [`FuturesContract`] instance.
168    #[allow(clippy::too_many_arguments)]
169    pub fn new(
170        id: InstrumentId,
171        raw_symbol: Symbol,
172        asset_class: AssetClass,
173        exchange: Option<Ustr>,
174        underlying: Ustr,
175        activation_ns: UnixNanos,
176        expiration_ns: UnixNanos,
177        currency: Currency,
178        price_precision: u8,
179        price_increment: Price,
180        multiplier: Quantity,
181        lot_size: Quantity,
182        max_quantity: Option<Quantity>,
183        min_quantity: Option<Quantity>,
184        max_price: Option<Price>,
185        min_price: Option<Price>,
186        margin_init: Option<Decimal>,
187        margin_maint: Option<Decimal>,
188        maker_fee: Option<Decimal>,
189        taker_fee: Option<Decimal>,
190        ts_event: UnixNanos,
191        ts_init: UnixNanos,
192    ) -> Self {
193        Self::new_checked(
194            id,
195            raw_symbol,
196            asset_class,
197            exchange,
198            underlying,
199            activation_ns,
200            expiration_ns,
201            currency,
202            price_precision,
203            price_increment,
204            multiplier,
205            lot_size,
206            max_quantity,
207            min_quantity,
208            max_price,
209            min_price,
210            margin_init,
211            margin_maint,
212            maker_fee,
213            taker_fee,
214            ts_event,
215            ts_init,
216        )
217        .expect(FAILED)
218    }
219}
220
221impl PartialEq<Self> for FuturesContract {
222    fn eq(&self, other: &Self) -> bool {
223        self.id == other.id
224    }
225}
226
227impl Eq for FuturesContract {}
228
229impl Hash for FuturesContract {
230    fn hash<H: Hasher>(&self, state: &mut H) {
231        self.id.hash(state);
232    }
233}
234
235impl Instrument for FuturesContract {
236    fn into_any(self) -> InstrumentAny {
237        InstrumentAny::FuturesContract(self)
238    }
239
240    fn id(&self) -> InstrumentId {
241        self.id
242    }
243
244    fn raw_symbol(&self) -> Symbol {
245        self.raw_symbol
246    }
247
248    fn asset_class(&self) -> AssetClass {
249        self.asset_class
250    }
251
252    fn instrument_class(&self) -> InstrumentClass {
253        InstrumentClass::Future
254    }
255    fn underlying(&self) -> Option<Ustr> {
256        Some(self.underlying)
257    }
258
259    fn base_currency(&self) -> Option<Currency> {
260        None
261    }
262
263    fn quote_currency(&self) -> Currency {
264        self.currency
265    }
266
267    fn settlement_currency(&self) -> Currency {
268        self.currency
269    }
270
271    fn isin(&self) -> Option<Ustr> {
272        None
273    }
274
275    fn option_kind(&self) -> Option<OptionKind> {
276        None
277    }
278
279    fn exchange(&self) -> Option<Ustr> {
280        self.exchange
281    }
282
283    fn strike_price(&self) -> Option<Price> {
284        None
285    }
286
287    fn activation_ns(&self) -> Option<UnixNanos> {
288        Some(self.activation_ns)
289    }
290
291    fn expiration_ns(&self) -> Option<UnixNanos> {
292        Some(self.expiration_ns)
293    }
294
295    fn is_inverse(&self) -> bool {
296        false
297    }
298
299    fn price_precision(&self) -> u8 {
300        self.price_precision
301    }
302
303    fn size_precision(&self) -> u8 {
304        0
305    }
306
307    fn price_increment(&self) -> Price {
308        self.price_increment
309    }
310
311    fn size_increment(&self) -> Quantity {
312        Quantity::from(1)
313    }
314
315    fn multiplier(&self) -> Quantity {
316        self.multiplier
317    }
318
319    fn lot_size(&self) -> Option<Quantity> {
320        Some(self.lot_size)
321    }
322
323    fn max_quantity(&self) -> Option<Quantity> {
324        self.max_quantity
325    }
326
327    fn min_quantity(&self) -> Option<Quantity> {
328        self.min_quantity
329    }
330
331    fn max_notional(&self) -> Option<Money> {
332        None
333    }
334
335    fn min_notional(&self) -> Option<Money> {
336        None
337    }
338
339    fn max_price(&self) -> Option<Price> {
340        self.max_price
341    }
342
343    fn min_price(&self) -> Option<Price> {
344        self.min_price
345    }
346
347    fn ts_event(&self) -> UnixNanos {
348        self.ts_event
349    }
350
351    fn ts_init(&self) -> UnixNanos {
352        self.ts_init
353    }
354}
355
356////////////////////////////////////////////////////////////////////////////////
357// Tests
358////////////////////////////////////////////////////////////////////////////////
359#[cfg(test)]
360mod tests {
361    use rstest::rstest;
362
363    use crate::instruments::stubs::*;
364
365    #[rstest]
366    fn test_equality() {
367        let futures_contract = futures_contract_es(None, None);
368        assert_eq!(futures_contract, futures_contract.clone());
369    }
370}