nautilus_model/instruments/
option_contract.rs

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