nautilus_model/instruments/
option_contract.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::{
21        FAILED, check_equal_u8, check_valid_string_ascii, check_valid_string_ascii_optional,
22    },
23};
24use rust_decimal::Decimal;
25use serde::{Deserialize, Serialize};
26use ustr::Ustr;
27
28use super::{Instrument, any::InstrumentAny};
29use crate::{
30    enums::{AssetClass, InstrumentClass, OptionKind},
31    identifiers::{InstrumentId, Symbol},
32    types::{
33        currency::Currency,
34        money::Money,
35        price::{Price, check_positive_price},
36        quantity::{Quantity, check_positive_quantity},
37    },
38};
39
40/// Represents a generic option contract instrument.
41#[repr(C)]
42#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
43#[cfg_attr(
44    feature = "python",
45    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
46)]
47pub struct OptionContract {
48    /// The instrument ID.
49    pub id: InstrumentId,
50    /// The raw/local/native symbol for the instrument, assigned by the venue.
51    pub raw_symbol: Symbol,
52    /// The option contract asset class.
53    pub asset_class: AssetClass,
54    /// The exchange ISO 10383 Market Identifier Code (MIC) where the instrument trades.
55    pub exchange: Option<Ustr>,
56    /// The underlying asset.
57    pub underlying: Ustr,
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 currency: Currency,
68    /// The price decimal precision.
69    pub price_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 trading size decimal precision.
75    pub size_precision: u8,
76    /// The option multiplier.
77    pub multiplier: Quantity,
78    /// The rounded lot unit size (standard/board).
79    pub lot_size: Quantity,
80    /// The initial (order) margin requirement in percentage of order value.
81    pub margin_init: Decimal,
82    /// The maintenance (position) margin in percentage of position value.
83    pub margin_maint: Decimal,
84    /// The fee rate for liquidity makers as a percentage of order value.
85    pub maker_fee: Decimal,
86    /// The fee rate for liquidity takers as a percentage of order value.
87    pub taker_fee: Decimal,
88    /// The maximum allowable order quantity.
89    pub max_quantity: Option<Quantity>,
90    /// The minimum allowable order quantity.
91    pub min_quantity: Option<Quantity>,
92    /// The maximum allowable quoted price.
93    pub max_price: Option<Price>,
94    /// The minimum allowable quoted price.
95    pub min_price: Option<Price>,
96    /// UNIX timestamp (nanoseconds) when the data event occurred.
97    pub ts_event: UnixNanos,
98    /// UNIX timestamp (nanoseconds) when the data object was initialized.
99    pub ts_init: UnixNanos,
100}
101
102impl OptionContract {
103    /// Creates a new [`OptionContract`] instance with correctness checking.
104    ///
105    /// # Notes
106    ///
107    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
108    /// # Errors
109    ///
110    /// Returns an error if any input validation fails.
111    #[allow(clippy::too_many_arguments)]
112    pub fn new_checked(
113        instrument_id: InstrumentId,
114        raw_symbol: Symbol,
115        asset_class: AssetClass,
116        exchange: Option<Ustr>,
117        underlying: Ustr,
118        option_kind: OptionKind,
119        strike_price: Price,
120        currency: Currency,
121        activation_ns: UnixNanos,
122        expiration_ns: UnixNanos,
123        price_precision: u8,
124        price_increment: Price,
125        multiplier: Quantity,
126        lot_size: Quantity,
127        max_quantity: Option<Quantity>,
128        min_quantity: Option<Quantity>,
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_valid_string_ascii_optional(exchange.map(|u| u.as_str()), stringify!(isin))?;
139        check_valid_string_ascii(underlying.as_str(), stringify!(underlying))?;
140        check_equal_u8(
141            price_precision,
142            price_increment.precision,
143            stringify!(price_precision),
144            stringify!(price_increment.precision),
145        )?;
146        check_positive_price(price_increment, stringify!(price_increment))?;
147        check_positive_quantity(multiplier, stringify!(multiplier))?;
148        check_positive_quantity(lot_size, stringify!(lot_size))?;
149
150        Ok(Self {
151            id: instrument_id,
152            raw_symbol,
153            asset_class,
154            exchange,
155            underlying,
156            option_kind,
157            activation_ns,
158            expiration_ns,
159            strike_price,
160            currency,
161            price_precision,
162            price_increment,
163            size_precision: 0,
164            size_increment: Quantity::from(1),
165            multiplier,
166            lot_size,
167            margin_init: margin_init.unwrap_or_default(),
168            margin_maint: margin_maint.unwrap_or_default(),
169            maker_fee: maker_fee.unwrap_or_default(),
170            taker_fee: taker_fee.unwrap_or_default(),
171            max_quantity,
172            min_quantity: Some(min_quantity.unwrap_or(1.into())),
173            max_price,
174            min_price,
175            ts_event,
176            ts_init,
177        })
178    }
179
180    /// Creates a new [`OptionContract`] instance.
181    ///
182    /// # Panics
183    ///
184    /// Panics if any input parameter is invalid (see `new_checked`).
185    #[allow(clippy::too_many_arguments)]
186    pub fn new(
187        instrument_id: InstrumentId,
188        raw_symbol: Symbol,
189        asset_class: AssetClass,
190        exchange: Option<Ustr>,
191        underlying: Ustr,
192        option_kind: OptionKind,
193        strike_price: Price,
194        currency: Currency,
195        activation_ns: UnixNanos,
196        expiration_ns: UnixNanos,
197        price_precision: u8,
198        price_increment: Price,
199        multiplier: Quantity,
200        lot_size: Quantity,
201        max_quantity: Option<Quantity>,
202        min_quantity: Option<Quantity>,
203        max_price: Option<Price>,
204        min_price: Option<Price>,
205        margin_init: Option<Decimal>,
206        margin_maint: Option<Decimal>,
207        maker_fee: Option<Decimal>,
208        taker_fee: Option<Decimal>,
209        ts_event: UnixNanos,
210        ts_init: UnixNanos,
211    ) -> Self {
212        Self::new_checked(
213            instrument_id,
214            raw_symbol,
215            asset_class,
216            exchange,
217            underlying,
218            option_kind,
219            strike_price,
220            currency,
221            activation_ns,
222            expiration_ns,
223            price_precision,
224            price_increment,
225            multiplier,
226            lot_size,
227            max_quantity,
228            min_quantity,
229            max_price,
230            min_price,
231            margin_init,
232            margin_maint,
233            maker_fee,
234            taker_fee,
235            ts_event,
236            ts_init,
237        )
238        .expect(FAILED)
239    }
240}
241
242impl PartialEq<Self> for OptionContract {
243    fn eq(&self, other: &Self) -> bool {
244        self.id == other.id
245    }
246}
247
248impl Eq for OptionContract {}
249
250impl Hash for OptionContract {
251    fn hash<H: Hasher>(&self, state: &mut H) {
252        self.id.hash(state);
253    }
254}
255
256impl Instrument for OptionContract {
257    fn into_any(self) -> InstrumentAny {
258        InstrumentAny::OptionContract(self)
259    }
260
261    fn id(&self) -> InstrumentId {
262        self.id
263    }
264
265    fn raw_symbol(&self) -> Symbol {
266        self.raw_symbol
267    }
268
269    fn asset_class(&self) -> AssetClass {
270        self.asset_class
271    }
272
273    fn instrument_class(&self) -> InstrumentClass {
274        InstrumentClass::Option
275    }
276    fn underlying(&self) -> Option<Ustr> {
277        Some(self.underlying)
278    }
279
280    fn base_currency(&self) -> Option<Currency> {
281        None
282    }
283
284    fn quote_currency(&self) -> Currency {
285        self.currency
286    }
287
288    fn settlement_currency(&self) -> Currency {
289        self.currency
290    }
291
292    fn isin(&self) -> Option<Ustr> {
293        None
294    }
295
296    fn option_kind(&self) -> Option<OptionKind> {
297        Some(self.option_kind)
298    }
299
300    fn exchange(&self) -> Option<Ustr> {
301        self.exchange
302    }
303
304    fn strike_price(&self) -> Option<Price> {
305        Some(self.strike_price)
306    }
307
308    fn activation_ns(&self) -> Option<UnixNanos> {
309        Some(self.activation_ns)
310    }
311
312    fn expiration_ns(&self) -> Option<UnixNanos> {
313        Some(self.expiration_ns)
314    }
315
316    fn is_inverse(&self) -> bool {
317        false
318    }
319
320    fn price_precision(&self) -> u8 {
321        self.price_precision
322    }
323
324    fn size_precision(&self) -> u8 {
325        0
326    }
327
328    fn price_increment(&self) -> Price {
329        self.price_increment
330    }
331
332    fn size_increment(&self) -> Quantity {
333        Quantity::from(1)
334    }
335
336    fn multiplier(&self) -> Quantity {
337        self.multiplier
338    }
339
340    fn lot_size(&self) -> Option<Quantity> {
341        Some(self.lot_size)
342    }
343
344    fn max_quantity(&self) -> Option<Quantity> {
345        self.max_quantity
346    }
347
348    fn min_quantity(&self) -> Option<Quantity> {
349        self.min_quantity
350    }
351
352    fn max_notional(&self) -> Option<Money> {
353        None
354    }
355
356    fn min_notional(&self) -> Option<Money> {
357        None
358    }
359
360    fn max_price(&self) -> Option<Price> {
361        self.max_price
362    }
363
364    fn min_price(&self) -> Option<Price> {
365        self.min_price
366    }
367
368    fn ts_event(&self) -> UnixNanos {
369        self.ts_event
370    }
371
372    fn ts_init(&self) -> UnixNanos {
373        self.ts_init
374    }
375}
376
377////////////////////////////////////////////////////////////////////////////////
378// Tests
379////////////////////////////////////////////////////////////////////////////////
380#[cfg(test)]
381mod tests {
382    use rstest::rstest;
383
384    use crate::instruments::{OptionContract, stubs::*};
385
386    #[rstest]
387    fn test_equality(option_contract_appl: OptionContract) {
388        let option_contract_appl2 = option_contract_appl;
389        assert_eq!(option_contract_appl, option_contract_appl2);
390    }
391}