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