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    correctness::{check_equal_u8, check_valid_string, check_valid_string_optional, FAILED},
20    UnixNanos,
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{any::InstrumentAny, Instrument};
27use crate::{
28    enums::{AssetClass, InstrumentClass, OptionKind},
29    identifiers::{InstrumentId, Symbol},
30    types::{
31        currency::Currency,
32        money::Money,
33        price::{check_positive_price, Price},
34        quantity::{check_positive_quantity, 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 OptionContract {
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 option contract asset class.
51    pub asset_class: AssetClass,
52    /// The exchange ISO 10383 Market Identifier Code (MIC) where the instrument trades.
53    pub exchange: Option<Ustr>,
54    /// The underlying asset.
55    pub underlying: Ustr,
56    /// The kind of option (PUT | CALL).
57    pub option_kind: OptionKind,
58    /// The option strike price.
59    pub strike_price: Price,
60    /// UNIX timestamp (nanoseconds) for contract activation.
61    pub activation_ns: UnixNanos,
62    /// UNIX timestamp (nanoseconds) for contract expiration.
63    pub expiration_ns: UnixNanos,
64    /// The option contract currency.
65    pub currency: Currency,
66    /// The price decimal precision.
67    pub price_precision: u8,
68    /// The minimum price increment (tick size).
69    pub price_increment: Price,
70    /// The minimum size increment.
71    pub size_increment: Quantity,
72    /// The trading size decimal precision.
73    pub size_precision: u8,
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 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 OptionContract {
101    /// Creates a new [`OptionContract`] instance with correctness checking.
102    ///
103    /// # Notes
104    ///
105    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
106    #[allow(clippy::too_many_arguments)]
107    pub fn new_checked(
108        id: InstrumentId,
109        raw_symbol: Symbol,
110        asset_class: AssetClass,
111        exchange: Option<Ustr>,
112        underlying: Ustr,
113        option_kind: OptionKind,
114        strike_price: Price,
115        currency: Currency,
116        activation_ns: UnixNanos,
117        expiration_ns: UnixNanos,
118        price_precision: u8,
119        price_increment: Price,
120        multiplier: Quantity,
121        lot_size: Quantity,
122        max_quantity: Option<Quantity>,
123        min_quantity: Option<Quantity>,
124        max_price: Option<Price>,
125        min_price: Option<Price>,
126        margin_init: Option<Decimal>,
127        margin_maint: Option<Decimal>,
128        maker_fee: Option<Decimal>,
129        taker_fee: Option<Decimal>,
130        ts_event: UnixNanos,
131        ts_init: UnixNanos,
132    ) -> anyhow::Result<Self> {
133        check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?;
134        check_valid_string(underlying.as_str(), stringify!(underlying))?;
135        check_equal_u8(
136            price_precision,
137            price_increment.precision,
138            stringify!(price_precision),
139            stringify!(price_increment.precision),
140        )?;
141        check_positive_price(price_increment.raw, stringify!(price_increment.raw))?;
142        check_positive_quantity(multiplier.raw, stringify!(multiplier.raw))?;
143        check_positive_quantity(lot_size.raw, stringify!(lot_size.raw))?;
144
145        Ok(Self {
146            id,
147            raw_symbol,
148            asset_class,
149            exchange,
150            underlying,
151            option_kind,
152            activation_ns,
153            expiration_ns,
154            strike_price,
155            currency,
156            price_precision,
157            price_increment,
158            size_precision: 0,
159            size_increment: Quantity::from(1),
160            multiplier,
161            lot_size,
162            margin_init: margin_init.unwrap_or_default(),
163            margin_maint: margin_maint.unwrap_or_default(),
164            maker_fee: maker_fee.unwrap_or_default(),
165            taker_fee: taker_fee.unwrap_or_default(),
166            max_quantity,
167            min_quantity: Some(min_quantity.unwrap_or(1.into())),
168            max_price,
169            min_price,
170            ts_event,
171            ts_init,
172        })
173    }
174
175    /// Creates a new [`OptionContract`] instance.
176    #[allow(clippy::too_many_arguments)]
177    pub fn new(
178        id: InstrumentId,
179        raw_symbol: Symbol,
180        asset_class: AssetClass,
181        exchange: Option<Ustr>,
182        underlying: Ustr,
183        option_kind: OptionKind,
184        strike_price: Price,
185        currency: Currency,
186        activation_ns: UnixNanos,
187        expiration_ns: UnixNanos,
188        price_precision: u8,
189        price_increment: Price,
190        multiplier: Quantity,
191        lot_size: Quantity,
192        max_quantity: Option<Quantity>,
193        min_quantity: Option<Quantity>,
194        max_price: Option<Price>,
195        min_price: Option<Price>,
196        margin_init: Option<Decimal>,
197        margin_maint: Option<Decimal>,
198        maker_fee: Option<Decimal>,
199        taker_fee: Option<Decimal>,
200        ts_event: UnixNanos,
201        ts_init: UnixNanos,
202    ) -> Self {
203        Self::new_checked(
204            id,
205            raw_symbol,
206            asset_class,
207            exchange,
208            underlying,
209            option_kind,
210            strike_price,
211            currency,
212            activation_ns,
213            expiration_ns,
214            price_precision,
215            price_increment,
216            multiplier,
217            lot_size,
218            max_quantity,
219            min_quantity,
220            max_price,
221            min_price,
222            margin_init,
223            margin_maint,
224            maker_fee,
225            taker_fee,
226            ts_event,
227            ts_init,
228        )
229        .expect(FAILED)
230    }
231}
232
233impl PartialEq<Self> for OptionContract {
234    fn eq(&self, other: &Self) -> bool {
235        self.id == other.id
236    }
237}
238
239impl Eq for OptionContract {}
240
241impl Hash for OptionContract {
242    fn hash<H: Hasher>(&self, state: &mut H) {
243        self.id.hash(state);
244    }
245}
246
247impl Instrument for OptionContract {
248    fn into_any(self) -> InstrumentAny {
249        InstrumentAny::OptionContract(self)
250    }
251
252    fn id(&self) -> InstrumentId {
253        self.id
254    }
255
256    fn raw_symbol(&self) -> Symbol {
257        self.raw_symbol
258    }
259
260    fn asset_class(&self) -> AssetClass {
261        self.asset_class
262    }
263
264    fn instrument_class(&self) -> InstrumentClass {
265        InstrumentClass::Option
266    }
267    fn underlying(&self) -> Option<Ustr> {
268        Some(self.underlying)
269    }
270
271    fn base_currency(&self) -> Option<Currency> {
272        None
273    }
274
275    fn quote_currency(&self) -> Currency {
276        self.currency
277    }
278
279    fn settlement_currency(&self) -> Currency {
280        self.currency
281    }
282
283    fn isin(&self) -> Option<Ustr> {
284        None
285    }
286
287    fn option_kind(&self) -> Option<OptionKind> {
288        Some(self.option_kind)
289    }
290
291    fn exchange(&self) -> Option<Ustr> {
292        self.exchange
293    }
294
295    fn strike_price(&self) -> Option<Price> {
296        Some(self.strike_price)
297    }
298
299    fn activation_ns(&self) -> Option<UnixNanos> {
300        Some(self.activation_ns)
301    }
302
303    fn expiration_ns(&self) -> Option<UnixNanos> {
304        Some(self.expiration_ns)
305    }
306
307    fn is_inverse(&self) -> bool {
308        false
309    }
310
311    fn price_precision(&self) -> u8 {
312        self.price_precision
313    }
314
315    fn size_precision(&self) -> u8 {
316        0
317    }
318
319    fn price_increment(&self) -> Price {
320        self.price_increment
321    }
322
323    fn size_increment(&self) -> Quantity {
324        Quantity::from(1)
325    }
326
327    fn multiplier(&self) -> Quantity {
328        self.multiplier
329    }
330
331    fn lot_size(&self) -> Option<Quantity> {
332        Some(self.lot_size)
333    }
334
335    fn max_quantity(&self) -> Option<Quantity> {
336        self.max_quantity
337    }
338
339    fn min_quantity(&self) -> Option<Quantity> {
340        self.min_quantity
341    }
342
343    fn max_notional(&self) -> Option<Money> {
344        None
345    }
346
347    fn min_notional(&self) -> Option<Money> {
348        None
349    }
350
351    fn max_price(&self) -> Option<Price> {
352        self.max_price
353    }
354
355    fn min_price(&self) -> Option<Price> {
356        self.min_price
357    }
358
359    fn ts_event(&self) -> UnixNanos {
360        self.ts_event
361    }
362
363    fn ts_init(&self) -> UnixNanos {
364        self.ts_init
365    }
366}
367
368////////////////////////////////////////////////////////////////////////////////
369// Tests
370////////////////////////////////////////////////////////////////////////////////
371#[cfg(test)]
372mod tests {
373    use rstest::rstest;
374
375    use crate::instruments::{stubs::*, OptionContract};
376
377    #[rstest]
378    fn test_equality(option_contract_appl: OptionContract) {
379        let option_contract_appl2 = option_contract_appl;
380        assert_eq!(option_contract_appl, option_contract_appl2);
381    }
382}