Skip to main content

nautilus_model/instruments/
crypto_option.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 generic option contract instrument.
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 CryptoOption {
50    /// The instrument ID.
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    /// The kind of option (PUT | CALL).
63    pub option_kind: OptionKind,
64    /// The option strike price.
65    pub strike_price: Price,
66    /// UNIX timestamp (nanoseconds) for contract activation.
67    pub activation_ns: UnixNanos,
68    /// UNIX timestamp (nanoseconds) for contract expiration.
69    pub expiration_ns: UnixNanos,
70    /// The option contract currency.
71    pub price_precision: u8,
72    /// The trading size decimal precision.
73    pub size_precision: u8,
74    /// The minimum price increment (tick size).
75    pub price_increment: Price,
76    /// The minimum size increment.
77    pub size_increment: Quantity,
78    /// The option multiplier.
79    pub multiplier: Quantity,
80    /// The rounded lot unit size (standard/board).
81    pub lot_size: Quantity,
82    /// The initial (order) margin requirement in percentage of order value.
83    pub margin_init: Decimal,
84    /// The maintenance (position) margin in percentage of position value.
85    pub margin_maint: Decimal,
86    /// The fee rate for liquidity makers as a percentage of order value.
87    pub maker_fee: Decimal,
88    /// The fee rate for liquidity takers as a percentage of order value.
89    pub taker_fee: Decimal,
90    /// The maximum allowable order quantity.
91    pub max_quantity: Option<Quantity>,
92    /// The minimum allowable order quantity.
93    pub min_quantity: Option<Quantity>,
94    /// The maximum allowable order notional value.
95    pub max_notional: Option<Money>,
96    /// The minimum allowable order notional value.
97    pub min_notional: Option<Money>,
98    /// The maximum allowable quoted price.
99    pub max_price: Option<Price>,
100    /// The minimum allowable quoted price.
101    pub min_price: Option<Price>,
102    /// Additional instrument metadata as a JSON-serializable dictionary.
103    pub info: Option<Params>,
104    /// UNIX timestamp (nanoseconds) when the data event occurred.
105    pub ts_event: UnixNanos,
106    /// UNIX timestamp (nanoseconds) when the data object was initialized.
107    pub ts_init: UnixNanos,
108}
109
110impl CryptoOption {
111    /// Creates a new [`CryptoOption`] instance with correctness checking.
112    ///
113    /// # Notes
114    ///
115    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
116    /// # Errors
117    ///
118    /// Returns an error if any input validation fails.
119    #[expect(clippy::too_many_arguments)]
120    pub fn new_checked(
121        instrument_id: InstrumentId,
122        raw_symbol: Symbol,
123        underlying: Currency,
124        quote_currency: Currency,
125        settlement_currency: Currency,
126        is_inverse: bool,
127        option_kind: OptionKind,
128        strike_price: Price,
129        activation_ns: UnixNanos,
130        expiration_ns: UnixNanos,
131        price_precision: u8,
132        size_precision: u8,
133        price_increment: Price,
134        size_increment: Quantity,
135        multiplier: Option<Quantity>,
136        lot_size: Option<Quantity>,
137        max_quantity: Option<Quantity>,
138        min_quantity: Option<Quantity>,
139        max_notional: Option<Money>,
140        min_notional: Option<Money>,
141        max_price: Option<Price>,
142        min_price: Option<Price>,
143        margin_init: Option<Decimal>,
144        margin_maint: Option<Decimal>,
145        maker_fee: Option<Decimal>,
146        taker_fee: Option<Decimal>,
147        info: Option<Params>,
148        ts_event: UnixNanos,
149        ts_init: UnixNanos,
150    ) -> CorrectnessResult<Self> {
151        check_equal_u8(
152            price_precision,
153            price_increment.precision,
154            stringify!(price_precision),
155            stringify!(price_increment.precision),
156        )?;
157        check_equal_u8(
158            size_precision,
159            size_increment.precision,
160            stringify!(size_precision),
161            stringify!(size_increment.precision),
162        )?;
163        check_positive_price(price_increment, stringify!(price_increment))?;
164        check_positive_quantity(size_increment, stringify!(size_increment))?;
165
166        if let Some(multiplier) = multiplier {
167            check_positive_quantity(multiplier, stringify!(multiplier))?;
168        }
169
170        Ok(Self {
171            id: instrument_id,
172            raw_symbol,
173            underlying,
174            quote_currency,
175            settlement_currency,
176            is_inverse,
177            option_kind,
178            strike_price,
179            activation_ns,
180            expiration_ns,
181            price_precision,
182            size_precision,
183            price_increment,
184            size_increment,
185            multiplier: multiplier.unwrap_or(Quantity::from(1)),
186            lot_size: lot_size.unwrap_or(Quantity::from(1)),
187            margin_init: margin_init.unwrap_or_default(),
188            margin_maint: margin_maint.unwrap_or_default(),
189            maker_fee: maker_fee.unwrap_or_default(),
190            taker_fee: taker_fee.unwrap_or_default(),
191            max_notional,
192            min_notional,
193            max_quantity,
194            min_quantity: Some(min_quantity.unwrap_or(1.into())),
195            max_price,
196            min_price,
197            info,
198            ts_event,
199            ts_init,
200        })
201    }
202
203    /// Creates a new [`CryptoOption`] instance.
204    ///
205    /// # Panics
206    ///
207    /// Panics if any parameter is invalid (see `new_checked`).
208    #[expect(clippy::too_many_arguments)]
209    #[must_use]
210    pub fn new(
211        instrument_id: InstrumentId,
212        raw_symbol: Symbol,
213        underlying: Currency,
214        quote_currency: Currency,
215        settlement_currency: Currency,
216        is_inverse: bool,
217        option_kind: OptionKind,
218        strike_price: Price,
219        activation_ns: UnixNanos,
220        expiration_ns: UnixNanos,
221        price_precision: u8,
222        size_precision: u8,
223        price_increment: Price,
224        size_increment: Quantity,
225        multiplier: Option<Quantity>,
226        lot_size: Option<Quantity>,
227        max_quantity: Option<Quantity>,
228        min_quantity: Option<Quantity>,
229        max_notional: Option<Money>,
230        min_notional: Option<Money>,
231        max_price: Option<Price>,
232        min_price: Option<Price>,
233        margin_init: Option<Decimal>,
234        margin_maint: Option<Decimal>,
235        maker_fee: Option<Decimal>,
236        taker_fee: Option<Decimal>,
237        info: Option<Params>,
238        ts_event: UnixNanos,
239        ts_init: UnixNanos,
240    ) -> Self {
241        Self::new_checked(
242            instrument_id,
243            raw_symbol,
244            underlying,
245            quote_currency,
246            settlement_currency,
247            is_inverse,
248            option_kind,
249            strike_price,
250            activation_ns,
251            expiration_ns,
252            price_precision,
253            size_precision,
254            price_increment,
255            size_increment,
256            multiplier,
257            lot_size,
258            max_quantity,
259            min_quantity,
260            max_notional,
261            min_notional,
262            max_price,
263            min_price,
264            margin_init,
265            margin_maint,
266            maker_fee,
267            taker_fee,
268            info,
269            ts_event,
270            ts_init,
271        )
272        .expect_display(FAILED)
273    }
274}
275
276impl PartialEq<Self> for CryptoOption {
277    fn eq(&self, other: &Self) -> bool {
278        self.id == other.id
279    }
280}
281
282impl Eq for CryptoOption {}
283
284impl Hash for CryptoOption {
285    fn hash<H: Hasher>(&self, state: &mut H) {
286        self.id.hash(state);
287    }
288}
289
290impl Instrument for CryptoOption {
291    fn into_any(self) -> InstrumentAny {
292        InstrumentAny::CryptoOption(self)
293    }
294
295    fn id(&self) -> InstrumentId {
296        self.id
297    }
298
299    fn raw_symbol(&self) -> Symbol {
300        self.raw_symbol
301    }
302
303    fn asset_class(&self) -> AssetClass {
304        AssetClass::Cryptocurrency
305    }
306
307    fn instrument_class(&self) -> InstrumentClass {
308        InstrumentClass::Option
309    }
310
311    fn underlying(&self) -> Option<Ustr> {
312        Some(self.underlying.code)
313    }
314
315    fn base_currency(&self) -> Option<Currency> {
316        Some(self.underlying)
317    }
318
319    fn quote_currency(&self) -> Currency {
320        self.quote_currency
321    }
322
323    fn settlement_currency(&self) -> Currency {
324        self.settlement_currency
325    }
326
327    fn is_inverse(&self) -> bool {
328        self.is_inverse
329    }
330
331    fn isin(&self) -> Option<Ustr> {
332        None // Not applicable
333    }
334
335    fn option_kind(&self) -> Option<OptionKind> {
336        Some(self.option_kind)
337    }
338
339    fn strike_price(&self) -> Option<Price> {
340        Some(self.strike_price)
341    }
342
343    fn activation_ns(&self) -> Option<UnixNanos> {
344        Some(self.activation_ns)
345    }
346
347    fn expiration_ns(&self) -> Option<UnixNanos> {
348        Some(self.expiration_ns)
349    }
350
351    fn exchange(&self) -> Option<Ustr> {
352        None // Not applicable (these are tradfi MICs)
353    }
354
355    fn price_precision(&self) -> u8 {
356        self.price_precision
357    }
358
359    fn size_precision(&self) -> u8 {
360        self.size_precision
361    }
362
363    fn price_increment(&self) -> Price {
364        self.price_increment
365    }
366
367    fn size_increment(&self) -> Quantity {
368        self.size_increment
369    }
370
371    fn multiplier(&self) -> Quantity {
372        self.multiplier
373    }
374
375    fn lot_size(&self) -> Option<Quantity> {
376        Some(self.lot_size)
377    }
378
379    fn max_quantity(&self) -> Option<Quantity> {
380        self.max_quantity
381    }
382
383    fn min_quantity(&self) -> Option<Quantity> {
384        self.min_quantity
385    }
386
387    fn max_notional(&self) -> Option<Money> {
388        self.max_notional
389    }
390
391    fn min_notional(&self) -> Option<Money> {
392        self.min_notional
393    }
394
395    fn max_price(&self) -> Option<Price> {
396        self.max_price
397    }
398
399    fn min_price(&self) -> Option<Price> {
400        self.min_price
401    }
402
403    fn ts_event(&self) -> UnixNanos {
404        self.ts_event
405    }
406
407    fn ts_init(&self) -> UnixNanos {
408        self.ts_init
409    }
410
411    fn margin_init(&self) -> Decimal {
412        self.margin_init
413    }
414
415    fn margin_maint(&self) -> Decimal {
416        self.margin_maint
417    }
418
419    fn maker_fee(&self) -> Decimal {
420        self.maker_fee
421    }
422
423    fn taker_fee(&self) -> Decimal {
424        self.taker_fee
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use rstest::rstest;
431
432    use crate::{
433        enums::{AssetClass, InstrumentClass, OptionKind},
434        identifiers::{InstrumentId, Symbol},
435        instruments::{CryptoOption, Instrument, stubs::*},
436        types::{Currency, Price, Quantity},
437    };
438
439    #[rstest]
440    fn test_trait_accessors(crypto_option_btc_deribit: CryptoOption) {
441        assert_eq!(
442            crypto_option_btc_deribit.id(),
443            InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"),
444        );
445        assert_eq!(
446            crypto_option_btc_deribit.asset_class(),
447            AssetClass::Cryptocurrency
448        );
449        assert_eq!(
450            crypto_option_btc_deribit.instrument_class(),
451            InstrumentClass::Option
452        );
453        assert_eq!(
454            crypto_option_btc_deribit.option_kind(),
455            Some(OptionKind::Put)
456        );
457        assert_eq!(
458            crypto_option_btc_deribit.strike_price(),
459            Some(Price::from("16000.000"))
460        );
461        assert!(!crypto_option_btc_deribit.is_inverse());
462        assert_eq!(crypto_option_btc_deribit.price_precision(), 3);
463        assert_eq!(crypto_option_btc_deribit.size_precision(), 1);
464        assert_eq!(
465            crypto_option_btc_deribit.min_quantity(),
466            Some(Quantity::from("0.1"))
467        );
468        assert!(crypto_option_btc_deribit.activation_ns().is_some());
469        assert!(crypto_option_btc_deribit.expiration_ns().is_some());
470    }
471
472    #[rstest]
473    fn test_new_checked_price_precision_mismatch() {
474        let result = CryptoOption::new_checked(
475            InstrumentId::from("TEST.DERIBIT"),
476            Symbol::from("TEST"),
477            Currency::BTC(),
478            Currency::USD(),
479            Currency::BTC(),
480            false,
481            OptionKind::Call,
482            Price::from("50000.0"),
483            0.into(),
484            0.into(),
485            4, // mismatch
486            1,
487            Price::from("0.001"),
488            Quantity::from("0.1"),
489            None,
490            None,
491            None,
492            None,
493            None,
494            None,
495            None,
496            None,
497            None,
498            None,
499            None,
500            None,
501            None,
502            0.into(),
503            0.into(),
504        );
505        assert!(result.is_err());
506    }
507
508    #[rstest]
509    fn test_serialization_roundtrip(crypto_option_btc_deribit: CryptoOption) {
510        let json = serde_json::to_string(&crypto_option_btc_deribit).unwrap();
511        let deserialized: CryptoOption = serde_json::from_str(&json).unwrap();
512        assert_eq!(crypto_option_btc_deribit, deserialized);
513    }
514}