nautilus_model/instruments/
futures_contract.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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    UnixNanos,
20    correctness::{FAILED, check_equal_u8, check_valid_string, check_valid_string_optional},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny};
27use crate::{
28    enums::{AssetClass, InstrumentClass, OptionKind},
29    identifiers::{InstrumentId, Symbol},
30    types::{
31        currency::Currency,
32        money::Money,
33        price::{Price, check_positive_price},
34        quantity::{Quantity, check_positive_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 = "posei_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    /// # Errors
103    ///
104    /// Returns an error if any input validation fails.
105    #[allow(clippy::too_many_arguments)]
106    pub fn new_checked(
107        instrument_id: InstrumentId,
108        raw_symbol: Symbol,
109        asset_class: AssetClass,
110        exchange: Option<Ustr>,
111        underlying: Ustr,
112        activation_ns: UnixNanos,
113        expiration_ns: UnixNanos,
114        currency: Currency,
115        price_precision: u8,
116        price_increment: Price,
117        multiplier: Quantity,
118        lot_size: Quantity,
119        max_quantity: Option<Quantity>,
120        min_quantity: Option<Quantity>,
121        max_price: Option<Price>,
122        min_price: Option<Price>,
123        margin_init: Option<Decimal>,
124        margin_maint: Option<Decimal>,
125        maker_fee: Option<Decimal>,
126        taker_fee: Option<Decimal>,
127        ts_event: UnixNanos,
128        ts_init: UnixNanos,
129    ) -> anyhow::Result<Self> {
130        check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?;
131        check_valid_string(underlying.as_str(), stringify!(underlying))?;
132        check_equal_u8(
133            price_precision,
134            price_increment.precision,
135            stringify!(price_precision),
136            stringify!(price_increment.precision),
137        )?;
138        check_positive_price(price_increment, stringify!(price_increment))?;
139        check_positive_quantity(multiplier, stringify!(multiplier))?;
140        check_positive_quantity(lot_size, stringify!(lot_size))?;
141
142        Ok(Self {
143            id: instrument_id,
144            raw_symbol,
145            asset_class,
146            exchange,
147            underlying,
148            activation_ns,
149            expiration_ns,
150            currency,
151            price_precision,
152            price_increment,
153            size_precision: 0,
154            size_increment: Quantity::from(1),
155            multiplier,
156            lot_size,
157            max_quantity,
158            min_quantity: Some(min_quantity.unwrap_or(1.into())),
159            max_price,
160            min_price,
161            margin_init: margin_init.unwrap_or_default(),
162            margin_maint: margin_maint.unwrap_or_default(),
163            maker_fee: maker_fee.unwrap_or_default(),
164            taker_fee: taker_fee.unwrap_or_default(),
165            ts_event,
166            ts_init,
167        })
168    }
169
170    /// Creates a new [`FuturesContract`] instance.
171    ///
172    /// # Panics
173    ///
174    /// Panics if any input parameter is invalid (see `new_checked`).
175    #[allow(clippy::too_many_arguments)]
176    pub fn new(
177        instrument_id: InstrumentId,
178        raw_symbol: Symbol,
179        asset_class: AssetClass,
180        exchange: Option<Ustr>,
181        underlying: Ustr,
182        activation_ns: UnixNanos,
183        expiration_ns: UnixNanos,
184        currency: Currency,
185        price_precision: u8,
186        price_increment: Price,
187        multiplier: Quantity,
188        lot_size: Quantity,
189        max_quantity: Option<Quantity>,
190        min_quantity: Option<Quantity>,
191        max_price: Option<Price>,
192        min_price: Option<Price>,
193        margin_init: Option<Decimal>,
194        margin_maint: Option<Decimal>,
195        maker_fee: Option<Decimal>,
196        taker_fee: Option<Decimal>,
197        ts_event: UnixNanos,
198        ts_init: UnixNanos,
199    ) -> Self {
200        Self::new_checked(
201            instrument_id,
202            raw_symbol,
203            asset_class,
204            exchange,
205            underlying,
206            activation_ns,
207            expiration_ns,
208            currency,
209            price_precision,
210            price_increment,
211            multiplier,
212            lot_size,
213            max_quantity,
214            min_quantity,
215            max_price,
216            min_price,
217            margin_init,
218            margin_maint,
219            maker_fee,
220            taker_fee,
221            ts_event,
222            ts_init,
223        )
224        .expect(FAILED)
225    }
226}
227
228impl PartialEq<Self> for FuturesContract {
229    fn eq(&self, other: &Self) -> bool {
230        self.id == other.id
231    }
232}
233
234impl Eq for FuturesContract {}
235
236impl Hash for FuturesContract {
237    fn hash<H: Hasher>(&self, state: &mut H) {
238        self.id.hash(state);
239    }
240}
241
242impl Instrument for FuturesContract {
243    fn into_any(self) -> InstrumentAny {
244        InstrumentAny::FuturesContract(self)
245    }
246
247    fn id(&self) -> InstrumentId {
248        self.id
249    }
250
251    fn raw_symbol(&self) -> Symbol {
252        self.raw_symbol
253    }
254
255    fn asset_class(&self) -> AssetClass {
256        self.asset_class
257    }
258
259    fn instrument_class(&self) -> InstrumentClass {
260        InstrumentClass::Future
261    }
262    fn underlying(&self) -> Option<Ustr> {
263        Some(self.underlying)
264    }
265
266    fn base_currency(&self) -> Option<Currency> {
267        None
268    }
269
270    fn quote_currency(&self) -> Currency {
271        self.currency
272    }
273
274    fn settlement_currency(&self) -> Currency {
275        self.currency
276    }
277
278    fn isin(&self) -> Option<Ustr> {
279        None
280    }
281
282    fn option_kind(&self) -> Option<OptionKind> {
283        None
284    }
285
286    fn exchange(&self) -> Option<Ustr> {
287        self.exchange
288    }
289
290    fn strike_price(&self) -> Option<Price> {
291        None
292    }
293
294    fn activation_ns(&self) -> Option<UnixNanos> {
295        Some(self.activation_ns)
296    }
297
298    fn expiration_ns(&self) -> Option<UnixNanos> {
299        Some(self.expiration_ns)
300    }
301
302    fn is_inverse(&self) -> bool {
303        false
304    }
305
306    fn price_precision(&self) -> u8 {
307        self.price_precision
308    }
309
310    fn size_precision(&self) -> u8 {
311        0
312    }
313
314    fn price_increment(&self) -> Price {
315        self.price_increment
316    }
317
318    fn size_increment(&self) -> Quantity {
319        Quantity::from(1)
320    }
321
322    fn multiplier(&self) -> Quantity {
323        self.multiplier
324    }
325
326    fn lot_size(&self) -> Option<Quantity> {
327        Some(self.lot_size)
328    }
329
330    fn max_quantity(&self) -> Option<Quantity> {
331        self.max_quantity
332    }
333
334    fn min_quantity(&self) -> Option<Quantity> {
335        self.min_quantity
336    }
337
338    fn max_notional(&self) -> Option<Money> {
339        None
340    }
341
342    fn min_notional(&self) -> Option<Money> {
343        None
344    }
345
346    fn max_price(&self) -> Option<Price> {
347        self.max_price
348    }
349
350    fn min_price(&self) -> Option<Price> {
351        self.min_price
352    }
353
354    fn ts_event(&self) -> UnixNanos {
355        self.ts_event
356    }
357
358    fn ts_init(&self) -> UnixNanos {
359        self.ts_init
360    }
361}
362
363////////////////////////////////////////////////////////////////////////////////
364// Tests
365////////////////////////////////////////////////////////////////////////////////
366#[cfg(test)]
367mod tests {
368    use rstest::rstest;
369
370    use crate::instruments::stubs::*;
371
372    #[rstest]
373    fn test_equality() {
374        let futures_contract = futures_contract_es(None, None);
375        assert_eq!(futures_contract, futures_contract.clone());
376    }
377}