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