Skip to main content

nautilus_model/instruments/
crypto_future.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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    Params, UnixNanos,
20    correctness::{CorrectnessResult, CorrectnessResultExt, FAILED, check_equal_u8},
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 deliverable futures contract instrument, with crypto assets as underlying and for settlement.
39#[repr(C)]
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
44)]
45#[cfg_attr(
46    feature = "python",
47    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
48)]
49pub struct CryptoFuture {
50    /// The instrument ID for the instrument.
51    pub id: InstrumentId,
52    /// The raw/local/native symbol for the instrument, assigned by the venue.
53    pub raw_symbol: Symbol,
54    /// The underlying asset.
55    pub underlying: Currency,
56    /// The contract quote currency.
57    pub quote_currency: Currency,
58    /// The settlement currency.
59    pub settlement_currency: Currency,
60    /// If the instrument costing is inverse (quantity expressed in quote currency units).
61    pub is_inverse: bool,
62    /// UNIX timestamp (nanoseconds) for contract activation.
63    pub activation_ns: UnixNanos,
64    /// UNIX timestamp (nanoseconds) for contract expiration.
65    pub expiration_ns: UnixNanos,
66    /// The price decimal precision.
67    pub price_precision: u8,
68    /// The trading size decimal precision.
69    pub size_precision: u8,
70    /// The minimum price increment (tick size).
71    pub price_increment: Price,
72    /// The minimum size increment.
73    pub size_increment: Quantity,
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 order notional value.
91    pub max_notional: Option<Money>,
92    /// The minimum allowable order notional value.
93    pub min_notional: Option<Money>,
94    /// The maximum allowable quoted price.
95    pub max_price: Option<Price>,
96    /// The minimum allowable quoted price.
97    pub min_price: Option<Price>,
98    /// Additional instrument metadata as a JSON-serializable dictionary.
99    pub info: Option<Params>,
100    /// UNIX timestamp (nanoseconds) when the data event occurred.
101    pub ts_event: UnixNanos,
102    /// UNIX timestamp (nanoseconds) when the data object was initialized.
103    pub ts_init: UnixNanos,
104}
105
106impl CryptoFuture {
107    /// Creates a new [`CryptoFuture`] instance with correctness checking.
108    ///
109    /// # Notes
110    ///
111    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
112    /// # Errors
113    ///
114    /// Returns an error if any input validation fails.
115    #[expect(clippy::too_many_arguments)]
116    pub fn new_checked(
117        instrument_id: InstrumentId,
118        raw_symbol: Symbol,
119        underlying: Currency,
120        quote_currency: Currency,
121        settlement_currency: Currency,
122        is_inverse: bool,
123        activation_ns: UnixNanos,
124        expiration_ns: UnixNanos,
125        price_precision: u8,
126        size_precision: u8,
127        price_increment: Price,
128        size_increment: Quantity,
129        multiplier: Option<Quantity>,
130        lot_size: Option<Quantity>,
131        max_quantity: Option<Quantity>,
132        min_quantity: Option<Quantity>,
133        max_notional: Option<Money>,
134        min_notional: Option<Money>,
135        max_price: Option<Price>,
136        min_price: Option<Price>,
137        margin_init: Option<Decimal>,
138        margin_maint: Option<Decimal>,
139        maker_fee: Option<Decimal>,
140        taker_fee: Option<Decimal>,
141        info: Option<Params>,
142        ts_event: UnixNanos,
143        ts_init: UnixNanos,
144    ) -> CorrectnessResult<Self> {
145        check_equal_u8(
146            price_precision,
147            price_increment.precision,
148            stringify!(price_precision),
149            stringify!(price_increment.precision),
150        )?;
151        check_equal_u8(
152            size_precision,
153            size_increment.precision,
154            stringify!(size_precision),
155            stringify!(size_increment.precision),
156        )?;
157        check_positive_price(price_increment, stringify!(price_increment))?;
158        check_positive_quantity(size_increment, stringify!(size_increment))?;
159
160        Ok(Self {
161            id: instrument_id,
162            raw_symbol,
163            underlying,
164            quote_currency,
165            settlement_currency,
166            is_inverse,
167            activation_ns,
168            expiration_ns,
169            price_precision,
170            size_precision,
171            price_increment,
172            size_increment,
173            multiplier: multiplier.unwrap_or(Quantity::from(1)),
174            lot_size: lot_size.unwrap_or(Quantity::from(1)),
175            margin_init: margin_init.unwrap_or_default(),
176            margin_maint: margin_maint.unwrap_or_default(),
177            maker_fee: maker_fee.unwrap_or_default(),
178            taker_fee: taker_fee.unwrap_or_default(),
179            max_quantity,
180            min_quantity,
181            max_notional,
182            min_notional,
183            max_price,
184            min_price,
185            info,
186            ts_event,
187            ts_init,
188        })
189    }
190
191    /// Creates a new [`CryptoFuture`] instance.
192    ///
193    /// # Panics
194    ///
195    /// Panics if any parameter is invalid (see `new_checked`).
196    #[expect(clippy::too_many_arguments)]
197    #[must_use]
198    pub fn new(
199        instrument_id: InstrumentId,
200        raw_symbol: Symbol,
201        underlying: Currency,
202        quote_currency: Currency,
203        settlement_currency: Currency,
204        is_inverse: bool,
205        activation_ns: UnixNanos,
206        expiration_ns: UnixNanos,
207        price_precision: u8,
208        size_precision: u8,
209        price_increment: Price,
210        size_increment: Quantity,
211        multiplier: Option<Quantity>,
212        lot_size: Option<Quantity>,
213        max_quantity: Option<Quantity>,
214        min_quantity: Option<Quantity>,
215        max_notional: Option<Money>,
216        min_notional: Option<Money>,
217        max_price: Option<Price>,
218        min_price: Option<Price>,
219        margin_init: Option<Decimal>,
220        margin_maint: Option<Decimal>,
221        maker_fee: Option<Decimal>,
222        taker_fee: Option<Decimal>,
223        info: Option<Params>,
224        ts_event: UnixNanos,
225        ts_init: UnixNanos,
226    ) -> Self {
227        Self::new_checked(
228            instrument_id,
229            raw_symbol,
230            underlying,
231            quote_currency,
232            settlement_currency,
233            is_inverse,
234            activation_ns,
235            expiration_ns,
236            price_precision,
237            size_precision,
238            price_increment,
239            size_increment,
240            multiplier,
241            lot_size,
242            max_quantity,
243            min_quantity,
244            max_notional,
245            min_notional,
246            max_price,
247            min_price,
248            margin_init,
249            margin_maint,
250            maker_fee,
251            taker_fee,
252            info,
253            ts_event,
254            ts_init,
255        )
256        .expect_display(FAILED)
257    }
258}
259
260impl PartialEq<Self> for CryptoFuture {
261    fn eq(&self, other: &Self) -> bool {
262        self.id == other.id
263    }
264}
265
266impl Eq for CryptoFuture {}
267
268impl Hash for CryptoFuture {
269    fn hash<H: Hasher>(&self, state: &mut H) {
270        self.id.hash(state);
271    }
272}
273
274impl Instrument for CryptoFuture {
275    fn into_any(self) -> InstrumentAny {
276        InstrumentAny::CryptoFuture(self)
277    }
278
279    fn id(&self) -> InstrumentId {
280        self.id
281    }
282
283    fn raw_symbol(&self) -> Symbol {
284        self.raw_symbol
285    }
286
287    fn asset_class(&self) -> AssetClass {
288        AssetClass::Cryptocurrency
289    }
290
291    fn instrument_class(&self) -> InstrumentClass {
292        InstrumentClass::Future
293    }
294
295    fn underlying(&self) -> Option<Ustr> {
296        Some(self.underlying.code)
297    }
298
299    fn base_currency(&self) -> Option<Currency> {
300        Some(self.underlying)
301    }
302
303    fn quote_currency(&self) -> Currency {
304        self.quote_currency
305    }
306
307    fn settlement_currency(&self) -> Currency {
308        self.settlement_currency
309    }
310
311    fn isin(&self) -> Option<Ustr> {
312        None
313    }
314
315    fn exchange(&self) -> Option<Ustr> {
316        None
317    }
318
319    fn option_kind(&self) -> Option<OptionKind> {
320        None
321    }
322
323    fn is_inverse(&self) -> bool {
324        self.is_inverse
325    }
326
327    fn price_precision(&self) -> u8 {
328        self.price_precision
329    }
330
331    fn size_precision(&self) -> u8 {
332        self.size_precision
333    }
334
335    fn price_increment(&self) -> Price {
336        self.price_increment
337    }
338
339    fn size_increment(&self) -> Quantity {
340        self.size_increment
341    }
342
343    fn multiplier(&self) -> Quantity {
344        self.multiplier
345    }
346
347    fn lot_size(&self) -> Option<Quantity> {
348        Some(self.lot_size)
349    }
350
351    fn max_quantity(&self) -> Option<Quantity> {
352        self.max_quantity
353    }
354
355    fn min_quantity(&self) -> Option<Quantity> {
356        self.min_quantity
357    }
358
359    fn max_price(&self) -> Option<Price> {
360        self.max_price
361    }
362
363    fn min_price(&self) -> Option<Price> {
364        self.min_price
365    }
366
367    fn ts_event(&self) -> UnixNanos {
368        self.ts_event
369    }
370
371    fn ts_init(&self) -> UnixNanos {
372        self.ts_init
373    }
374
375    fn margin_init(&self) -> Decimal {
376        self.margin_init
377    }
378
379    fn margin_maint(&self) -> Decimal {
380        self.margin_maint
381    }
382
383    fn maker_fee(&self) -> Decimal {
384        self.maker_fee
385    }
386
387    fn taker_fee(&self) -> Decimal {
388        self.taker_fee
389    }
390
391    fn strike_price(&self) -> Option<Price> {
392        None
393    }
394
395    fn activation_ns(&self) -> Option<UnixNanos> {
396        Some(self.activation_ns)
397    }
398
399    fn expiration_ns(&self) -> Option<UnixNanos> {
400        Some(self.expiration_ns)
401    }
402
403    fn max_notional(&self) -> Option<Money> {
404        self.max_notional
405    }
406
407    fn min_notional(&self) -> Option<Money> {
408        self.min_notional
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use rstest::rstest;
415
416    use crate::{
417        enums::{AssetClass, InstrumentClass},
418        identifiers::{InstrumentId, Symbol},
419        instruments::{CryptoFuture, Instrument, stubs::*},
420        types::{Currency, Price, Quantity},
421    };
422
423    #[rstest]
424    fn test_trait_accessors(crypto_future_btcusdt: CryptoFuture) {
425        assert_eq!(
426            crypto_future_btcusdt.id(),
427            InstrumentId::from("ETHUSDT-123.BINANCE")
428        );
429        assert_eq!(
430            crypto_future_btcusdt.asset_class(),
431            AssetClass::Cryptocurrency
432        );
433        assert_eq!(
434            crypto_future_btcusdt.instrument_class(),
435            InstrumentClass::Future
436        );
437        assert_eq!(crypto_future_btcusdt.quote_currency(), Currency::USDT());
438        assert_eq!(
439            crypto_future_btcusdt.settlement_currency(),
440            Currency::USDT()
441        );
442        assert!(!crypto_future_btcusdt.is_inverse());
443        assert_eq!(crypto_future_btcusdt.price_precision(), 2);
444        assert_eq!(crypto_future_btcusdt.size_precision(), 6);
445        assert!(crypto_future_btcusdt.activation_ns().is_some());
446        assert!(crypto_future_btcusdt.expiration_ns().is_some());
447    }
448
449    #[rstest]
450    fn test_new_checked_price_precision_mismatch() {
451        let result = CryptoFuture::new_checked(
452            InstrumentId::from("TEST.BINANCE"),
453            Symbol::from("TEST"),
454            Currency::BTC(),
455            Currency::USDT(),
456            Currency::USDT(),
457            false,
458            0.into(),
459            0.into(),
460            4, // mismatch
461            6,
462            Price::from("0.01"),
463            Quantity::from("0.000001"),
464            None,
465            None,
466            None,
467            None,
468            None,
469            None,
470            None,
471            None,
472            None,
473            None,
474            None,
475            None,
476            None,
477            0.into(),
478            0.into(),
479        );
480        assert!(result.is_err());
481    }
482
483    #[rstest]
484    fn test_serialization_roundtrip(crypto_future_btcusdt: CryptoFuture) {
485        let json = serde_json::to_string(&crypto_future_btcusdt).unwrap();
486        let deserialized: CryptoFuture = serde_json::from_str(&json).unwrap();
487        assert_eq!(crypto_future_btcusdt, deserialized);
488    }
489}