Skip to main content

nautilus_model/instruments/
binary_option.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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    Params, UnixNanos,
20    correctness::{CorrectnessResult, CorrectnessResultExt, 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 binary option instrument.
39#[repr(C)]
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
44)]
45#[cfg_attr(
46    feature = "python",
47    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
48)]
49pub struct BinaryOption {
50    /// The instrument ID.
51    pub id: InstrumentId,
52    /// The raw/local/native symbol for the instrument, assigned by the venue.
53    pub raw_symbol: Symbol,
54    /// The binary option asset class.
55    pub asset_class: AssetClass,
56    /// The binary option contract currency.
57    pub currency: Currency,
58    /// UNIX timestamp (nanoseconds) for contract activation.
59    pub activation_ns: UnixNanos,
60    /// UNIX timestamp (nanoseconds) for contract expiration.
61    pub expiration_ns: UnixNanos,
62    /// The price decimal precision.
63    pub price_precision: u8,
64    /// The trading size decimal precision.
65    pub size_precision: u8,
66    /// The minimum price increment (tick size).
67    pub price_increment: Price,
68    /// The minimum size increment.
69    pub size_increment: Quantity,
70    /// The initial (order) margin requirement in percentage of order value.
71    pub margin_init: Decimal,
72    /// The maintenance (position) margin in percentage of position value.
73    pub margin_maint: Decimal,
74    /// The fee rate for liquidity makers as a percentage of order value.
75    pub maker_fee: Decimal,
76    /// The fee rate for liquidity takers as a percentage of order value.
77    pub taker_fee: Decimal,
78    /// The binary outcome of the market.
79    pub outcome: Option<Ustr>,
80    /// The market description.
81    pub description: Option<Ustr>,
82    /// The maximum allowable order quantity.
83    pub max_quantity: Option<Quantity>,
84    /// The minimum allowable order quantity.
85    pub min_quantity: Option<Quantity>,
86    /// The maximum allowable order notional value.
87    pub max_notional: Option<Money>,
88    /// The minimum allowable order notional value.
89    pub min_notional: Option<Money>,
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    /// Additional instrument metadata as a JSON-serializable dictionary.
95    pub info: Option<Params>,
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 BinaryOption {
103    /// Creates a new [`BinaryOption`] 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 (e.g., invalid precision or increments).
111    #[expect(clippy::too_many_arguments)]
112    pub fn new_checked(
113        instrument_id: InstrumentId,
114        raw_symbol: Symbol,
115        asset_class: AssetClass,
116        currency: Currency,
117        activation_ns: UnixNanos,
118        expiration_ns: UnixNanos,
119        price_precision: u8,
120        size_precision: u8,
121        price_increment: Price,
122        size_increment: Quantity,
123        outcome: Option<Ustr>,
124        description: Option<Ustr>,
125        max_quantity: Option<Quantity>,
126        min_quantity: Option<Quantity>,
127        max_notional: Option<Money>,
128        min_notional: Option<Money>,
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        info: Option<Params>,
136        ts_event: UnixNanos,
137        ts_init: UnixNanos,
138    ) -> CorrectnessResult<Self> {
139        check_equal_u8(
140            price_precision,
141            price_increment.precision,
142            stringify!(price_precision),
143            stringify!(price_increment.precision),
144        )?;
145        check_equal_u8(
146            size_precision,
147            size_increment.precision,
148            stringify!(size_precision),
149            stringify!(size_increment.precision),
150        )?;
151        check_positive_price(price_increment, stringify!(price_increment))?;
152        check_positive_quantity(size_increment, stringify!(size_increment))?;
153
154        Ok(Self {
155            id: instrument_id,
156            raw_symbol,
157            asset_class,
158            currency,
159            activation_ns,
160            expiration_ns,
161            price_precision,
162            size_precision,
163            price_increment,
164            size_increment,
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            outcome,
170            description,
171            max_quantity,
172            min_quantity,
173            max_notional,
174            min_notional,
175            max_price,
176            min_price,
177            info,
178            ts_event,
179            ts_init,
180        })
181    }
182
183    /// Creates a new [`BinaryOption`] instance by validating parameters.
184    ///
185    /// # Panics
186    ///
187    /// Panics if parameter validation fails during `new_checked`.
188    #[expect(clippy::too_many_arguments)]
189    #[must_use]
190    pub fn new(
191        instrument_id: InstrumentId,
192        raw_symbol: Symbol,
193        asset_class: AssetClass,
194        currency: Currency,
195        activation_ns: UnixNanos,
196        expiration_ns: UnixNanos,
197        price_precision: u8,
198        size_precision: u8,
199        price_increment: Price,
200        size_increment: Quantity,
201        outcome: Option<Ustr>,
202        description: Option<Ustr>,
203        max_quantity: Option<Quantity>,
204        min_quantity: Option<Quantity>,
205        max_notional: Option<Money>,
206        min_notional: Option<Money>,
207        max_price: Option<Price>,
208        min_price: Option<Price>,
209        margin_init: Option<Decimal>,
210        margin_maint: Option<Decimal>,
211        maker_fee: Option<Decimal>,
212        taker_fee: Option<Decimal>,
213        info: Option<Params>,
214        ts_event: UnixNanos,
215        ts_init: UnixNanos,
216    ) -> Self {
217        Self::new_checked(
218            instrument_id,
219            raw_symbol,
220            asset_class,
221            currency,
222            activation_ns,
223            expiration_ns,
224            price_precision,
225            size_precision,
226            price_increment,
227            size_increment,
228            outcome,
229            description,
230            max_quantity,
231            min_quantity,
232            max_notional,
233            min_notional,
234            max_price,
235            min_price,
236            margin_init,
237            margin_maint,
238            maker_fee,
239            taker_fee,
240            info,
241            ts_event,
242            ts_init,
243        )
244        .expect_display(FAILED)
245    }
246}
247
248impl PartialEq<Self> for BinaryOption {
249    fn eq(&self, other: &Self) -> bool {
250        self.id == other.id
251    }
252}
253
254impl Eq for BinaryOption {}
255
256impl Hash for BinaryOption {
257    fn hash<H: Hasher>(&self, state: &mut H) {
258        self.id.hash(state);
259    }
260}
261
262impl Instrument for BinaryOption {
263    fn into_any(self) -> InstrumentAny {
264        InstrumentAny::BinaryOption(self)
265    }
266
267    fn id(&self) -> InstrumentId {
268        self.id
269    }
270
271    fn raw_symbol(&self) -> Symbol {
272        self.raw_symbol
273    }
274
275    fn asset_class(&self) -> AssetClass {
276        self.asset_class
277    }
278
279    fn instrument_class(&self) -> InstrumentClass {
280        InstrumentClass::BinaryOption
281    }
282
283    fn underlying(&self) -> Option<Ustr> {
284        None
285    }
286
287    fn base_currency(&self) -> Option<Currency> {
288        None
289    }
290
291    fn quote_currency(&self) -> Currency {
292        self.currency
293    }
294
295    fn settlement_currency(&self) -> Currency {
296        self.currency
297    }
298
299    fn isin(&self) -> Option<Ustr> {
300        None
301    }
302
303    fn exchange(&self) -> Option<Ustr> {
304        None
305    }
306
307    fn option_kind(&self) -> Option<OptionKind> {
308        None
309    }
310
311    fn is_inverse(&self) -> bool {
312        false
313    }
314
315    fn price_precision(&self) -> u8 {
316        self.price_precision
317    }
318
319    fn size_precision(&self) -> u8 {
320        self.size_precision
321    }
322
323    fn price_increment(&self) -> Price {
324        self.price_increment
325    }
326
327    fn size_increment(&self) -> Quantity {
328        self.size_increment
329    }
330
331    fn multiplier(&self) -> Quantity {
332        Quantity::from(1)
333    }
334
335    fn lot_size(&self) -> Option<Quantity> {
336        Some(Quantity::from(1))
337    }
338
339    fn max_quantity(&self) -> Option<Quantity> {
340        self.max_quantity
341    }
342
343    fn min_quantity(&self) -> Option<Quantity> {
344        self.min_quantity
345    }
346
347    fn max_price(&self) -> Option<Price> {
348        self.max_price
349    }
350
351    fn min_price(&self) -> Option<Price> {
352        self.min_price
353    }
354
355    fn ts_event(&self) -> UnixNanos {
356        self.ts_event
357    }
358
359    fn ts_init(&self) -> UnixNanos {
360        self.ts_init
361    }
362
363    fn margin_init(&self) -> Decimal {
364        self.margin_init
365    }
366
367    fn margin_maint(&self) -> Decimal {
368        self.margin_maint
369    }
370
371    fn maker_fee(&self) -> Decimal {
372        self.maker_fee
373    }
374
375    fn taker_fee(&self) -> Decimal {
376        self.taker_fee
377    }
378
379    fn strike_price(&self) -> Option<Price> {
380        None
381    }
382
383    fn activation_ns(&self) -> Option<UnixNanos> {
384        Some(self.activation_ns)
385    }
386
387    fn expiration_ns(&self) -> Option<UnixNanos> {
388        Some(self.expiration_ns)
389    }
390
391    fn max_notional(&self) -> Option<Money> {
392        self.max_notional
393    }
394
395    fn min_notional(&self) -> Option<Money> {
396        self.min_notional
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use rstest::rstest;
403
404    use crate::{
405        enums::{AssetClass, InstrumentClass},
406        identifiers::{InstrumentId, Symbol},
407        instruments::{BinaryOption, Instrument, stubs::*},
408        types::{Currency, Price, Quantity},
409    };
410
411    #[rstest]
412    fn test_trait_accessors(binary_option: BinaryOption) {
413        assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
414        assert_eq!(
415            binary_option.instrument_class(),
416            InstrumentClass::BinaryOption
417        );
418        assert_eq!(binary_option.quote_currency(), Currency::USDC());
419        assert!(!binary_option.is_inverse());
420        assert_eq!(binary_option.price_precision(), 3);
421        assert_eq!(binary_option.size_precision(), 2);
422        assert!(binary_option.activation_ns().is_some());
423        assert!(binary_option.expiration_ns().is_some());
424    }
425
426    #[rstest]
427    fn test_new_checked_price_precision_mismatch() {
428        let result = BinaryOption::new_checked(
429            InstrumentId::from("TEST.POLYMARKET"),
430            Symbol::from("TEST"),
431            AssetClass::Alternative,
432            Currency::USDC(),
433            0.into(),
434            0.into(),
435            4, // mismatch
436            2,
437            Price::from("0.001"),
438            Quantity::from("0.01"),
439            None,
440            None,
441            None,
442            None,
443            None,
444            None,
445            None,
446            None,
447            None,
448            None,
449            None,
450            None,
451            None,
452            0.into(),
453            0.into(),
454        );
455        assert!(result.is_err());
456    }
457
458    #[rstest]
459    fn test_serialization_roundtrip(binary_option: BinaryOption) {
460        let json = serde_json::to_string(&binary_option).unwrap();
461        let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
462        assert_eq!(binary_option, deserialized);
463    }
464}