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, check_equal_u8},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny, tick_scheme::check_tick_scheme};
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.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 venue-assigned identifier of the event containing the instrument's market.
79    #[serde(default)]
80    pub event_id: Option<Ustr>,
81    /// The binary outcome of the market.
82    pub outcome: Option<Ustr>,
83    /// The market description.
84    pub description: Option<Ustr>,
85    /// The maximum allowable order quantity.
86    pub max_quantity: Option<Quantity>,
87    /// The minimum allowable order quantity.
88    pub min_quantity: Option<Quantity>,
89    /// The maximum allowable order notional value.
90    pub max_notional: Option<Money>,
91    /// The minimum allowable order notional value.
92    pub min_notional: Option<Money>,
93    /// The maximum allowable quoted price.
94    pub max_price: Option<Price>,
95    /// The minimum allowable quoted price.
96    pub min_price: Option<Price>,
97    /// The registered variable tick scheme name.
98    pub tick_scheme: Option<Ustr>,
99    /// Additional instrument metadata as a JSON-serializable dictionary.
100    pub info: Option<Params>,
101    /// UNIX timestamp (nanoseconds) when the data event occurred.
102    pub ts_event: UnixNanos,
103    /// UNIX timestamp (nanoseconds) when the data object was initialized.
104    pub ts_init: UnixNanos,
105}
106
107#[bon::bon]
108impl BinaryOption {
109    #[expect(clippy::too_many_arguments)]
110    fn new_checked(
111        instrument_id: InstrumentId,
112        raw_symbol: Symbol,
113        asset_class: AssetClass,
114        currency: Currency,
115        activation_ns: UnixNanos,
116        expiration_ns: UnixNanos,
117        price_precision: u8,
118        size_precision: u8,
119        price_increment: Price,
120        size_increment: Quantity,
121        event_id: Option<Ustr>,
122        outcome: Option<Ustr>,
123        description: Option<Ustr>,
124        max_quantity: Option<Quantity>,
125        min_quantity: Option<Quantity>,
126        max_notional: Option<Money>,
127        min_notional: Option<Money>,
128        max_price: Option<Price>,
129        min_price: Option<Price>,
130        margin_init: Option<Decimal>,
131        margin_maint: Option<Decimal>,
132        maker_fee: Option<Decimal>,
133        taker_fee: Option<Decimal>,
134        tick_scheme: Option<Ustr>,
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        check_tick_scheme(tick_scheme)?;
154
155        Ok(Self {
156            id: instrument_id,
157            raw_symbol,
158            asset_class,
159            currency,
160            activation_ns,
161            expiration_ns,
162            price_precision,
163            size_precision,
164            price_increment,
165            size_increment,
166            margin_init: margin_init.unwrap_or_default(),
167            margin_maint: margin_maint.unwrap_or_default(),
168            maker_fee: maker_fee.unwrap_or_default(),
169            taker_fee: taker_fee.unwrap_or_default(),
170            event_id,
171            outcome,
172            description,
173            max_quantity,
174            min_quantity,
175            max_notional,
176            min_notional,
177            max_price,
178            min_price,
179            tick_scheme,
180            info,
181            ts_event,
182            ts_init,
183        })
184    }
185
186    /// Returns a fluent builder for a [`BinaryOption`] instance.
187    ///
188    /// Required fields are enforced at compile time; optional fields can be omitted and use the
189    /// same defaults as checked construction. The same correctness checks run on `build`.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if any input validation fails.
194    #[builder(start_fn = builder, finish_fn = build)]
195    pub fn build_checked(
196        instrument_id: InstrumentId,
197        raw_symbol: Symbol,
198        asset_class: AssetClass,
199        currency: Currency,
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        event_id: Option<Ustr>,
207        outcome: Option<Ustr>,
208        description: Option<Ustr>,
209        max_quantity: Option<Quantity>,
210        min_quantity: Option<Quantity>,
211        max_notional: Option<Money>,
212        min_notional: Option<Money>,
213        max_price: Option<Price>,
214        min_price: Option<Price>,
215        margin_init: Option<Decimal>,
216        margin_maint: Option<Decimal>,
217        maker_fee: Option<Decimal>,
218        taker_fee: Option<Decimal>,
219        tick_scheme: Option<Ustr>,
220        info: Option<Params>,
221        ts_event: UnixNanos,
222        ts_init: UnixNanos,
223    ) -> CorrectnessResult<Self> {
224        Self::new_checked(
225            instrument_id,
226            raw_symbol,
227            asset_class,
228            currency,
229            activation_ns,
230            expiration_ns,
231            price_precision,
232            size_precision,
233            price_increment,
234            size_increment,
235            event_id,
236            outcome,
237            description,
238            max_quantity,
239            min_quantity,
240            max_notional,
241            min_notional,
242            max_price,
243            min_price,
244            margin_init,
245            margin_maint,
246            maker_fee,
247            taker_fee,
248            tick_scheme,
249            info,
250            ts_event,
251            ts_init,
252        )
253    }
254}
255
256impl PartialEq<Self> for BinaryOption {
257    fn eq(&self, other: &Self) -> bool {
258        self.id == other.id
259    }
260}
261
262impl Eq for BinaryOption {}
263
264impl Hash for BinaryOption {
265    fn hash<H: Hasher>(&self, state: &mut H) {
266        self.id.hash(state);
267    }
268}
269
270impl Instrument for BinaryOption {
271    fn into_any(self) -> InstrumentAny {
272        InstrumentAny::BinaryOption(self)
273    }
274
275    fn id(&self) -> InstrumentId {
276        self.id
277    }
278
279    fn raw_symbol(&self) -> Symbol {
280        self.raw_symbol
281    }
282
283    fn asset_class(&self) -> AssetClass {
284        self.asset_class
285    }
286
287    fn instrument_class(&self) -> InstrumentClass {
288        InstrumentClass::BinaryOption
289    }
290
291    fn underlying(&self) -> Option<Ustr> {
292        None
293    }
294
295    fn base_currency(&self) -> Option<Currency> {
296        None
297    }
298
299    fn quote_currency(&self) -> Currency {
300        self.currency
301    }
302
303    fn settlement_currency(&self) -> Currency {
304        self.currency
305    }
306
307    fn isin(&self) -> Option<Ustr> {
308        None
309    }
310
311    fn exchange(&self) -> Option<Ustr> {
312        None
313    }
314
315    fn option_kind(&self) -> Option<OptionKind> {
316        None
317    }
318
319    fn is_inverse(&self) -> bool {
320        false
321    }
322
323    fn price_precision(&self) -> u8 {
324        self.price_precision
325    }
326
327    fn size_precision(&self) -> u8 {
328        self.size_precision
329    }
330
331    fn price_increment(&self) -> Price {
332        self.price_increment
333    }
334
335    fn size_increment(&self) -> Quantity {
336        self.size_increment
337    }
338
339    fn multiplier(&self) -> Quantity {
340        Quantity::from(1)
341    }
342
343    fn lot_size(&self) -> Option<Quantity> {
344        Some(Quantity::from(1))
345    }
346
347    fn max_quantity(&self) -> Option<Quantity> {
348        self.max_quantity
349    }
350
351    fn min_quantity(&self) -> Option<Quantity> {
352        self.min_quantity
353    }
354
355    fn max_price(&self) -> Option<Price> {
356        self.max_price
357    }
358
359    fn min_price(&self) -> Option<Price> {
360        self.min_price
361    }
362
363    fn tick_scheme(&self) -> Option<Ustr> {
364        self.tick_scheme
365    }
366
367    fn info(&self) -> Option<&Params> {
368        self.info.as_ref()
369    }
370
371    fn ts_event(&self) -> UnixNanos {
372        self.ts_event
373    }
374
375    fn ts_init(&self) -> UnixNanos {
376        self.ts_init
377    }
378
379    fn margin_init(&self) -> Decimal {
380        self.margin_init
381    }
382
383    fn margin_maint(&self) -> Decimal {
384        self.margin_maint
385    }
386
387    fn maker_fee(&self) -> Decimal {
388        self.maker_fee
389    }
390
391    fn taker_fee(&self) -> Decimal {
392        self.taker_fee
393    }
394
395    fn strike_price(&self) -> Option<Price> {
396        None
397    }
398
399    fn activation_ns(&self) -> Option<UnixNanos> {
400        Some(self.activation_ns)
401    }
402
403    fn expiration_ns(&self) -> Option<UnixNanos> {
404        Some(self.expiration_ns)
405    }
406
407    fn max_notional(&self) -> Option<Money> {
408        self.max_notional
409    }
410
411    fn min_notional(&self) -> Option<Money> {
412        self.min_notional
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use rstest::rstest;
419    use rust_decimal_macros::dec;
420
421    use crate::{
422        enums::{AssetClass, InstrumentClass},
423        identifiers::{InstrumentId, Symbol},
424        instruments::{BinaryOption, Instrument, stubs::*},
425        types::{Currency, Money, Price, Quantity},
426    };
427
428    #[rstest]
429    fn test_trait_accessors(binary_option: BinaryOption) {
430        assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
431        assert_eq!(
432            binary_option.instrument_class(),
433            InstrumentClass::BinaryOption
434        );
435        assert_eq!(binary_option.quote_currency(), Currency::USDC());
436        assert!(!binary_option.is_inverse());
437        assert_eq!(binary_option.price_precision(), 3);
438        assert_eq!(binary_option.size_precision(), 2);
439        assert!(binary_option.activation_ns().is_some());
440        assert!(binary_option.expiration_ns().is_some());
441    }
442
443    #[rstest]
444    fn test_new_checked_price_precision_mismatch() {
445        let result = BinaryOption::new_checked(
446            InstrumentId::from("TEST.POLYMARKET"),
447            Symbol::from("TEST"),
448            AssetClass::Alternative,
449            Currency::USDC(),
450            0.into(),
451            0.into(),
452            4, // mismatch
453            2,
454            Price::from("0.001"),
455            Quantity::from("0.01"),
456            None,
457            None,
458            None,
459            None,
460            None,
461            None,
462            None,
463            None,
464            None,
465            None,
466            None,
467            None,
468            None,
469            None,
470            None,
471            0.into(),
472            0.into(),
473        );
474        assert!(result.is_err());
475    }
476
477    #[rstest]
478    fn test_event_id_serialization_and_legacy_default(mut binary_option: BinaryOption) {
479        binary_option.event_id = Some("event-123".into());
480        let mut value = serde_json::to_value(&binary_option).unwrap();
481        let restored: BinaryOption = serde_json::from_value(value.clone()).unwrap();
482        assert_eq!(restored.event_id, binary_option.event_id);
483        value.as_object_mut().unwrap().remove("event_id");
484        let restored: BinaryOption = serde_json::from_value(value).unwrap();
485        assert_eq!(restored.event_id, None);
486    }
487
488    #[rstest]
489    fn test_serialization_roundtrip(binary_option: BinaryOption) {
490        let json = serde_json::to_string(&binary_option).unwrap();
491        let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
492        assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
493    }
494
495    #[rstest]
496    fn test_builder_matches_new_checked() {
497        let positional = BinaryOption::new_checked(
498            InstrumentId::from("TEST.POLYMARKET"),
499            Symbol::from("TEST"),
500            AssetClass::Alternative,
501            Currency::USDC(),
502            1.into(),
503            2.into(),
504            3,
505            2,
506            Price::from("0.001"),
507            Quantity::from("0.01"),
508            Some("event-123".into()),
509            Some("Yes".into()),
510            Some("Will it happen?".into()),
511            Some(Quantity::from("10000.00")),
512            Some(Quantity::from("5.00")),
513            Some(Money::from("100000 USDC")),
514            Some(Money::from("10 USDC")),
515            Some(Price::from("0.999")),
516            Some(Price::from("0.001")),
517            Some(dec!(0.01)),
518            Some(dec!(0.02)),
519            Some(dec!(0.0002)),
520            Some(dec!(0.0004)),
521            None,
522            None,
523            3.into(),
524            4.into(),
525        )
526        .unwrap();
527
528        let built = BinaryOption::builder()
529            .instrument_id(InstrumentId::from("TEST.POLYMARKET"))
530            .raw_symbol(Symbol::from("TEST"))
531            .asset_class(AssetClass::Alternative)
532            .currency(Currency::USDC())
533            .activation_ns(1.into())
534            .expiration_ns(2.into())
535            .price_precision(3)
536            .size_precision(2)
537            .price_increment(Price::from("0.001"))
538            .size_increment(Quantity::from("0.01"))
539            .event_id("event-123".into())
540            .outcome("Yes".into())
541            .description("Will it happen?".into())
542            .max_quantity(Quantity::from("10000.00"))
543            .min_quantity(Quantity::from("5.00"))
544            .max_notional(Money::from("100000 USDC"))
545            .min_notional(Money::from("10 USDC"))
546            .max_price(Price::from("0.999"))
547            .min_price(Price::from("0.001"))
548            .margin_init(dec!(0.01))
549            .margin_maint(dec!(0.02))
550            .maker_fee(dec!(0.0002))
551            .taker_fee(dec!(0.0004))
552            .ts_event(3.into())
553            .ts_init(4.into())
554            .build()
555            .unwrap();
556
557        assert_eq!(
558            serde_json::to_value(&positional).unwrap(),
559            serde_json::to_value(&built).unwrap(),
560        );
561    }
562}