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    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 spread 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 FuturesSpread {
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 spread 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    /// The strategy type for the spread.
57    pub strategy_type: Ustr,
58    /// UNIX timestamp (nanoseconds) for contract activation.
59    pub activation_ns: UnixNanos,
60    /// UNIX timestamp (nanoseconds) for contract expiration.
61    pub expiration_ns: UnixNanos,
62    /// The futures spread currency.
63    pub currency: Currency,
64    /// The price decimal precision.
65    pub price_precision: u8,
66    /// The minimum price increment (tick size).
67    pub price_increment: Price,
68    /// The minimum size increment.
69    pub size_increment: Quantity,
70    /// The trading size decimal precision.
71    pub size_precision: u8,
72    /// The contract multiplier.
73    pub multiplier: Quantity,
74    /// The rounded lot unit size (standard/board).
75    pub lot_size: Quantity,
76    /// The initial (order) margin requirement in percentage of order value.
77    pub margin_init: Decimal,
78    /// The maintenance (position) margin in percentage of position value.
79    pub margin_maint: Decimal,
80    /// The fee rate for liquidity makers as a percentage of order value.
81    pub maker_fee: Decimal,
82    /// The fee rate for liquidity takers as a percentage of order value.
83    pub taker_fee: Decimal,
84    /// The maximum allowable order quantity.
85    pub max_quantity: Option<Quantity>,
86    /// The minimum allowable order quantity.
87    pub min_quantity: Option<Quantity>,
88    /// The maximum allowable quoted price.
89    pub max_price: Option<Price>,
90    /// The minimum allowable quoted price.
91    pub min_price: Option<Price>,
92    /// UNIX timestamp (nanoseconds) when the data event occurred.
93    pub ts_event: UnixNanos,
94    /// UNIX timestamp (nanoseconds) when the data object was initialized.
95    pub ts_init: UnixNanos,
96}
97
98impl FuturesSpread {
99    /// Creates a new [`FuturesSpread`] instance with correctness checking.
100    ///
101    /// # Notes
102    ///
103    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
104    #[allow(clippy::too_many_arguments)]
105    pub fn new_checked(
106        id: InstrumentId,
107        raw_symbol: Symbol,
108        asset_class: AssetClass,
109        exchange: Option<Ustr>,
110        underlying: Ustr,
111        strategy_type: 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(strategy_type.as_str(), stringify!(strategy_type))?;
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.raw, stringify!(price_increment.raw))?;
139        check_positive_quantity(multiplier.raw, stringify!(multiplier.raw))?;
140        check_positive_quantity(lot_size.raw, stringify!(lot_size.raw))?;
141
142        Ok(Self {
143            id,
144            raw_symbol,
145            asset_class,
146            exchange,
147            underlying,
148            strategy_type,
149            activation_ns,
150            expiration_ns,
151            currency,
152            price_precision,
153            price_increment,
154            size_precision: 0,
155            size_increment: Quantity::from("1"),
156            multiplier,
157            lot_size,
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            max_quantity,
163            min_quantity: Some(min_quantity.unwrap_or(1.into())),
164            max_price,
165            min_price,
166            ts_event,
167            ts_init,
168        })
169    }
170
171    /// Creates a new [`FuturesSpread`] instance.
172    #[allow(clippy::too_many_arguments)]
173    pub fn new(
174        id: InstrumentId,
175        raw_symbol: Symbol,
176        asset_class: AssetClass,
177        exchange: Option<Ustr>,
178        underlying: Ustr,
179        strategy_type: Ustr,
180        activation_ns: UnixNanos,
181        expiration_ns: UnixNanos,
182        currency: Currency,
183        price_precision: u8,
184        price_increment: Price,
185        multiplier: Quantity,
186        lot_size: Quantity,
187        max_quantity: Option<Quantity>,
188        min_quantity: Option<Quantity>,
189        max_price: Option<Price>,
190        min_price: Option<Price>,
191        margin_init: Option<Decimal>,
192        margin_maint: Option<Decimal>,
193        maker_fee: Option<Decimal>,
194        taker_fee: Option<Decimal>,
195        ts_event: UnixNanos,
196        ts_init: UnixNanos,
197    ) -> Self {
198        Self::new_checked(
199            id,
200            raw_symbol,
201            asset_class,
202            exchange,
203            underlying,
204            strategy_type,
205            activation_ns,
206            expiration_ns,
207            currency,
208            price_precision,
209            price_increment,
210            multiplier,
211            lot_size,
212            max_quantity,
213            min_quantity,
214            max_price,
215            min_price,
216            margin_init,
217            margin_maint,
218            maker_fee,
219            taker_fee,
220            ts_event,
221            ts_init,
222        )
223        .expect(FAILED)
224    }
225}
226
227impl PartialEq<Self> for FuturesSpread {
228    fn eq(&self, other: &Self) -> bool {
229        self.id == other.id
230    }
231}
232
233impl Eq for FuturesSpread {}
234
235impl Hash for FuturesSpread {
236    fn hash<H: Hasher>(&self, state: &mut H) {
237        self.id.hash(state);
238    }
239}
240
241impl Instrument for FuturesSpread {
242    fn into_any(self) -> InstrumentAny {
243        InstrumentAny::FuturesSpread(self)
244    }
245
246    fn id(&self) -> InstrumentId {
247        self.id
248    }
249
250    fn raw_symbol(&self) -> Symbol {
251        self.raw_symbol
252    }
253
254    fn asset_class(&self) -> AssetClass {
255        self.asset_class
256    }
257
258    fn instrument_class(&self) -> InstrumentClass {
259        InstrumentClass::FuturesSpread
260    }
261    fn underlying(&self) -> Option<Ustr> {
262        Some(self.underlying)
263    }
264
265    fn base_currency(&self) -> Option<Currency> {
266        None
267    }
268
269    fn quote_currency(&self) -> Currency {
270        self.currency
271    }
272
273    fn settlement_currency(&self) -> Currency {
274        self.currency
275    }
276
277    fn isin(&self) -> Option<Ustr> {
278        None
279    }
280
281    fn option_kind(&self) -> Option<OptionKind> {
282        None
283    }
284
285    fn exchange(&self) -> Option<Ustr> {
286        self.exchange
287    }
288
289    fn strike_price(&self) -> Option<Price> {
290        None
291    }
292
293    fn activation_ns(&self) -> Option<UnixNanos> {
294        Some(self.activation_ns)
295    }
296
297    fn expiration_ns(&self) -> Option<UnixNanos> {
298        Some(self.expiration_ns)
299    }
300
301    fn is_inverse(&self) -> bool {
302        false
303    }
304
305    fn price_precision(&self) -> u8 {
306        self.price_precision
307    }
308
309    fn size_precision(&self) -> u8 {
310        0
311    }
312
313    fn price_increment(&self) -> Price {
314        self.price_increment
315    }
316
317    fn size_increment(&self) -> Quantity {
318        Quantity::from(1)
319    }
320
321    fn multiplier(&self) -> Quantity {
322        self.multiplier
323    }
324
325    fn lot_size(&self) -> Option<Quantity> {
326        Some(self.lot_size)
327    }
328
329    fn max_quantity(&self) -> Option<Quantity> {
330        self.max_quantity
331    }
332
333    fn min_quantity(&self) -> Option<Quantity> {
334        self.min_quantity
335    }
336
337    fn max_notional(&self) -> Option<Money> {
338        None
339    }
340
341    fn min_notional(&self) -> Option<Money> {
342        None
343    }
344
345    fn max_price(&self) -> Option<Price> {
346        self.max_price
347    }
348
349    fn min_price(&self) -> Option<Price> {
350        self.min_price
351    }
352
353    fn ts_event(&self) -> UnixNanos {
354        self.ts_event
355    }
356
357    fn ts_init(&self) -> UnixNanos {
358        self.ts_init
359    }
360}
361
362////////////////////////////////////////////////////////////////////////////////
363// Tests
364////////////////////////////////////////////////////////////////////////////////
365#[cfg(test)]
366mod tests {
367    use rstest::rstest;
368
369    use crate::instruments::{stubs::*, FuturesSpread};
370
371    #[rstest]
372    fn test_equality(futures_spread_es: FuturesSpread) {
373        assert_eq!(futures_spread_es, futures_spread_es.clone());
374    }
375}