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