nautilus_model/instruments/
crypto_option.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    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 generic option contract 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 CryptoOption {
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 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    /// The kind of option (PUT | CALL).
59    pub option_kind: OptionKind,
60    /// The option strike price.
61    pub strike_price: Price,
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 option contract currency.
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 option 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    /// UNIX timestamp (nanoseconds) when the data event occurred.
99    pub ts_event: UnixNanos,
100    /// UNIX timestamp (nanoseconds) when the data object was initialized.
101    pub ts_init: UnixNanos,
102}
103
104impl CryptoOption {
105    /// Creates a new [`CryptoOption`] instance with correctness checking.
106    ///
107    /// # Notes
108    ///
109    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
110    /// # Errors
111    ///
112    /// Returns an error if any input validation fails.
113    #[allow(clippy::too_many_arguments)]
114    pub fn new_checked(
115        instrument_id: InstrumentId,
116        raw_symbol: Symbol,
117        underlying: Currency,
118        quote_currency: Currency,
119        settlement_currency: Currency,
120        is_inverse: bool,
121        option_kind: OptionKind,
122        strike_price: Price,
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        ts_event: UnixNanos,
142        ts_init: UnixNanos,
143    ) -> anyhow::Result<Self> {
144        check_equal_u8(
145            price_precision,
146            price_increment.precision,
147            stringify!(price_precision),
148            stringify!(price_increment.precision),
149        )?;
150        check_equal_u8(
151            size_precision,
152            size_increment.precision,
153            stringify!(size_precision),
154            stringify!(size_increment.precision),
155        )?;
156        check_positive_price(price_increment, stringify!(price_increment))?;
157        if let Some(multiplier) = multiplier {
158            check_positive_quantity(multiplier, stringify!(multiplier))?;
159        }
160
161        Ok(Self {
162            id: instrument_id,
163            raw_symbol,
164            underlying,
165            quote_currency,
166            settlement_currency,
167            is_inverse,
168            option_kind,
169            strike_price,
170            activation_ns,
171            expiration_ns,
172            price_precision,
173            size_precision,
174            price_increment,
175            size_increment,
176            multiplier: multiplier.unwrap_or(Quantity::from(1)),
177            lot_size: lot_size.unwrap_or(Quantity::from(1)),
178            margin_init: margin_init.unwrap_or_default(),
179            margin_maint: margin_maint.unwrap_or_default(),
180            maker_fee: maker_fee.unwrap_or_default(),
181            taker_fee: taker_fee.unwrap_or_default(),
182            max_notional,
183            min_notional,
184            max_quantity,
185            min_quantity: Some(min_quantity.unwrap_or(1.into())),
186            max_price,
187            min_price,
188            ts_event,
189            ts_init,
190        })
191    }
192
193    /// Creates a new [`CryptoOption`] instance.
194    ///
195    /// # Panics
196    ///
197    /// Panics if any parameter is invalid (see `new_checked`).
198    #[allow(clippy::too_many_arguments)]
199    pub fn new(
200        instrument_id: InstrumentId,
201        raw_symbol: Symbol,
202        underlying: Currency,
203        quote_currency: Currency,
204        settlement_currency: Currency,
205        is_inverse: bool,
206        option_kind: OptionKind,
207        strike_price: Price,
208        activation_ns: UnixNanos,
209        expiration_ns: UnixNanos,
210        price_precision: u8,
211        size_precision: u8,
212        price_increment: Price,
213        size_increment: Quantity,
214        multiplier: Option<Quantity>,
215        lot_size: Option<Quantity>,
216        max_quantity: Option<Quantity>,
217        min_quantity: Option<Quantity>,
218        max_notional: Option<Money>,
219        min_notional: Option<Money>,
220        max_price: Option<Price>,
221        min_price: Option<Price>,
222        margin_init: Option<Decimal>,
223        margin_maint: Option<Decimal>,
224        maker_fee: Option<Decimal>,
225        taker_fee: Option<Decimal>,
226        ts_event: UnixNanos,
227        ts_init: UnixNanos,
228    ) -> Self {
229        Self::new_checked(
230            instrument_id,
231            raw_symbol,
232            underlying,
233            quote_currency,
234            settlement_currency,
235            is_inverse,
236            option_kind,
237            strike_price,
238            activation_ns,
239            expiration_ns,
240            price_precision,
241            size_precision,
242            price_increment,
243            size_increment,
244            multiplier,
245            lot_size,
246            max_quantity,
247            min_quantity,
248            max_notional,
249            min_notional,
250            max_price,
251            min_price,
252            margin_init,
253            margin_maint,
254            maker_fee,
255            taker_fee,
256            ts_event,
257            ts_init,
258        )
259        .expect(FAILED)
260    }
261}
262
263impl PartialEq<Self> for CryptoOption {
264    fn eq(&self, other: &Self) -> bool {
265        self.id == other.id
266    }
267}
268
269impl Eq for CryptoOption {}
270
271impl Hash for CryptoOption {
272    fn hash<H: Hasher>(&self, state: &mut H) {
273        self.id.hash(state);
274    }
275}
276
277impl Instrument for CryptoOption {
278    fn into_any(self) -> InstrumentAny {
279        InstrumentAny::CryptoOption(self)
280    }
281
282    fn id(&self) -> InstrumentId {
283        self.id
284    }
285
286    fn raw_symbol(&self) -> Symbol {
287        self.raw_symbol
288    }
289
290    fn asset_class(&self) -> AssetClass {
291        AssetClass::Cryptocurrency
292    }
293
294    fn instrument_class(&self) -> InstrumentClass {
295        InstrumentClass::Option
296    }
297
298    fn underlying(&self) -> Option<Ustr> {
299        Some(self.underlying.code)
300    }
301
302    fn base_currency(&self) -> Option<Currency> {
303        Some(self.underlying)
304    }
305
306    fn quote_currency(&self) -> Currency {
307        self.quote_currency
308    }
309
310    fn settlement_currency(&self) -> Currency {
311        self.settlement_currency
312    }
313
314    fn is_inverse(&self) -> bool {
315        self.is_inverse
316    }
317
318    fn isin(&self) -> Option<Ustr> {
319        None // Not applicable
320    }
321
322    fn option_kind(&self) -> Option<OptionKind> {
323        Some(self.option_kind)
324    }
325
326    fn strike_price(&self) -> Option<Price> {
327        Some(self.strike_price)
328    }
329
330    fn activation_ns(&self) -> Option<UnixNanos> {
331        Some(self.activation_ns)
332    }
333
334    fn expiration_ns(&self) -> Option<UnixNanos> {
335        Some(self.expiration_ns)
336    }
337
338    fn exchange(&self) -> Option<Ustr> {
339        None // Not applicable (these are tradfi MICs)
340    }
341
342    fn price_precision(&self) -> u8 {
343        self.price_precision
344    }
345
346    fn size_precision(&self) -> u8 {
347        self.size_precision
348    }
349
350    fn price_increment(&self) -> Price {
351        self.price_increment
352    }
353
354    fn size_increment(&self) -> Quantity {
355        self.size_increment
356    }
357
358    fn multiplier(&self) -> Quantity {
359        self.multiplier
360    }
361
362    fn lot_size(&self) -> Option<Quantity> {
363        Some(self.lot_size)
364    }
365
366    fn max_quantity(&self) -> Option<Quantity> {
367        self.max_quantity
368    }
369
370    fn min_quantity(&self) -> Option<Quantity> {
371        self.min_quantity
372    }
373
374    fn max_notional(&self) -> Option<Money> {
375        self.max_notional
376    }
377
378    fn min_notional(&self) -> Option<Money> {
379        self.min_notional
380    }
381
382    fn max_price(&self) -> Option<Price> {
383        self.max_price
384    }
385
386    fn min_price(&self) -> Option<Price> {
387        self.min_price
388    }
389
390    fn ts_event(&self) -> UnixNanos {
391        self.ts_event
392    }
393
394    fn ts_init(&self) -> UnixNanos {
395        self.ts_init
396    }
397}
398
399////////////////////////////////////////////////////////////////////////////////
400// Tests
401////////////////////////////////////////////////////////////////////////////////
402#[cfg(test)]
403mod tests {
404    use rstest::rstest;
405
406    use crate::instruments::{CryptoOption, stubs::*};
407
408    #[rstest]
409    fn test_equality(crypto_option_btc_deribit: CryptoOption) {
410        let crypto_option = crypto_option_btc_deribit;
411        assert_eq!(crypto_option_btc_deribit, crypto_option);
412    }
413}