nautilus_model/instruments/
crypto_future.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},
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, Copy, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.model")
44)]
45pub struct CryptoFuture {
46    /// The instrument ID for the instrument.
47    pub id: InstrumentId,
48    /// The raw/local/native symbol for the instrument, assigned by the venue.
49    pub raw_symbol: Symbol,
50    /// The underlying asset.
51    pub underlying: Currency,
52    /// The contract quote currency.
53    pub quote_currency: Currency,
54    /// The settlement currency.
55    pub settlement_currency: Currency,
56    /// If the instrument costing is inverse (quantity expressed in quote currency units).
57    pub is_inverse: bool,
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 price decimal precision.
63    pub price_precision: u8,
64    /// The trading size decimal precision.
65    pub size_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 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 order notional value.
87    pub max_notional: Option<Money>,
88    /// The minimum allowable order notional value.
89    pub min_notional: Option<Money>,
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 CryptoFuture {
101    /// Creates a new [`CryptoFuture`] 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        underlying: Currency,
114        quote_currency: Currency,
115        settlement_currency: Currency,
116        is_inverse: bool,
117        activation_ns: UnixNanos,
118        expiration_ns: UnixNanos,
119        price_precision: u8,
120        size_precision: u8,
121        price_increment: Price,
122        size_increment: Quantity,
123        multiplier: Option<Quantity>,
124        lot_size: Option<Quantity>,
125        max_quantity: Option<Quantity>,
126        min_quantity: Option<Quantity>,
127        max_notional: Option<Money>,
128        min_notional: Option<Money>,
129        max_price: Option<Price>,
130        min_price: Option<Price>,
131        margin_init: Option<Decimal>,
132        margin_maint: Option<Decimal>,
133        maker_fee: Option<Decimal>,
134        taker_fee: Option<Decimal>,
135        ts_event: UnixNanos,
136        ts_init: UnixNanos,
137    ) -> anyhow::Result<Self> {
138        check_equal_u8(
139            price_precision,
140            price_increment.precision,
141            stringify!(price_precision),
142            stringify!(price_increment.precision),
143        )?;
144        check_equal_u8(
145            size_precision,
146            size_increment.precision,
147            stringify!(size_precision),
148            stringify!(size_increment.precision),
149        )?;
150        check_positive_price(price_increment, stringify!(price_increment))?;
151        check_positive_quantity(size_increment, stringify!(size_increment))?;
152
153        Ok(Self {
154            id: instrument_id,
155            raw_symbol,
156            underlying,
157            quote_currency,
158            settlement_currency,
159            is_inverse,
160            activation_ns,
161            expiration_ns,
162            price_precision,
163            size_precision,
164            price_increment,
165            size_increment,
166            multiplier: multiplier.unwrap_or(Quantity::from(1)),
167            lot_size: lot_size.unwrap_or(Quantity::from(1)),
168            margin_init: margin_init.unwrap_or_default(),
169            margin_maint: margin_maint.unwrap_or_default(),
170            maker_fee: maker_fee.unwrap_or_default(),
171            taker_fee: taker_fee.unwrap_or_default(),
172            max_quantity,
173            min_quantity,
174            max_notional,
175            min_notional,
176            max_price,
177            min_price,
178            ts_event,
179            ts_init,
180        })
181    }
182
183    /// Creates a new [`CryptoFuture`] instance.
184    ///
185    /// # Panics
186    ///
187    /// Panics if any parameter is invalid (see `new_checked`).
188    #[allow(clippy::too_many_arguments)]
189    pub fn new(
190        instrument_id: InstrumentId,
191        raw_symbol: Symbol,
192        underlying: Currency,
193        quote_currency: Currency,
194        settlement_currency: Currency,
195        is_inverse: bool,
196        activation_ns: UnixNanos,
197        expiration_ns: UnixNanos,
198        price_precision: u8,
199        size_precision: u8,
200        price_increment: Price,
201        size_increment: Quantity,
202        multiplier: Option<Quantity>,
203        lot_size: Option<Quantity>,
204        max_quantity: Option<Quantity>,
205        min_quantity: Option<Quantity>,
206        max_notional: Option<Money>,
207        min_notional: Option<Money>,
208        max_price: Option<Price>,
209        min_price: Option<Price>,
210        margin_init: Option<Decimal>,
211        margin_maint: Option<Decimal>,
212        maker_fee: Option<Decimal>,
213        taker_fee: Option<Decimal>,
214        ts_event: UnixNanos,
215        ts_init: UnixNanos,
216    ) -> Self {
217        Self::new_checked(
218            instrument_id,
219            raw_symbol,
220            underlying,
221            quote_currency,
222            settlement_currency,
223            is_inverse,
224            activation_ns,
225            expiration_ns,
226            price_precision,
227            size_precision,
228            price_increment,
229            size_increment,
230            multiplier,
231            lot_size,
232            max_quantity,
233            min_quantity,
234            max_notional,
235            min_notional,
236            max_price,
237            min_price,
238            margin_init,
239            margin_maint,
240            maker_fee,
241            taker_fee,
242            ts_event,
243            ts_init,
244        )
245        .expect(FAILED)
246    }
247}
248
249impl PartialEq<Self> for CryptoFuture {
250    fn eq(&self, other: &Self) -> bool {
251        self.id == other.id
252    }
253}
254
255impl Eq for CryptoFuture {}
256
257impl Hash for CryptoFuture {
258    fn hash<H: Hasher>(&self, state: &mut H) {
259        self.id.hash(state);
260    }
261}
262
263impl Instrument for CryptoFuture {
264    fn into_any(self) -> InstrumentAny {
265        InstrumentAny::CryptoFuture(self)
266    }
267
268    fn id(&self) -> InstrumentId {
269        self.id
270    }
271
272    fn raw_symbol(&self) -> Symbol {
273        self.raw_symbol
274    }
275
276    fn asset_class(&self) -> AssetClass {
277        AssetClass::Cryptocurrency
278    }
279
280    fn instrument_class(&self) -> InstrumentClass {
281        InstrumentClass::Future
282    }
283
284    fn underlying(&self) -> Option<Ustr> {
285        Some(self.underlying.code)
286    }
287
288    fn base_currency(&self) -> Option<Currency> {
289        Some(self.underlying)
290    }
291
292    fn quote_currency(&self) -> Currency {
293        self.quote_currency
294    }
295
296    fn settlement_currency(&self) -> Currency {
297        self.settlement_currency
298    }
299
300    fn isin(&self) -> Option<Ustr> {
301        None
302    }
303
304    fn exchange(&self) -> Option<Ustr> {
305        None
306    }
307
308    fn option_kind(&self) -> Option<OptionKind> {
309        None
310    }
311
312    fn is_inverse(&self) -> bool {
313        self.is_inverse
314    }
315
316    fn price_precision(&self) -> u8 {
317        self.price_precision
318    }
319
320    fn size_precision(&self) -> u8 {
321        self.size_precision
322    }
323
324    fn price_increment(&self) -> Price {
325        self.price_increment
326    }
327
328    fn size_increment(&self) -> Quantity {
329        self.size_increment
330    }
331
332    fn multiplier(&self) -> Quantity {
333        Quantity::from(1)
334    }
335
336    fn lot_size(&self) -> Option<Quantity> {
337        Some(self.lot_size)
338    }
339
340    fn max_quantity(&self) -> Option<Quantity> {
341        self.max_quantity
342    }
343
344    fn min_quantity(&self) -> Option<Quantity> {
345        self.min_quantity
346    }
347
348    fn max_price(&self) -> Option<Price> {
349        self.max_price
350    }
351
352    fn min_price(&self) -> Option<Price> {
353        self.min_price
354    }
355
356    fn ts_event(&self) -> UnixNanos {
357        self.ts_event
358    }
359
360    fn ts_init(&self) -> UnixNanos {
361        self.ts_init
362    }
363
364    fn strike_price(&self) -> Option<Price> {
365        None
366    }
367
368    fn activation_ns(&self) -> Option<UnixNanos> {
369        Some(self.activation_ns)
370    }
371
372    fn expiration_ns(&self) -> Option<UnixNanos> {
373        Some(self.expiration_ns)
374    }
375
376    fn max_notional(&self) -> Option<Money> {
377        self.max_notional
378    }
379
380    fn min_notional(&self) -> Option<Money> {
381        self.min_notional
382    }
383}
384
385////////////////////////////////////////////////////////////////////////////////
386// Tests
387////////////////////////////////////////////////////////////////////////////////
388#[cfg(test)]
389mod tests {
390    use rstest::rstest;
391
392    use crate::instruments::{CryptoFuture, stubs::*};
393
394    #[rstest]
395    fn test_equality(crypto_future_btcusdt: CryptoFuture) {
396        let cloned = crypto_future_btcusdt;
397        assert_eq!(crypto_future_btcusdt, cloned);
398    }
399}