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 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    /// The registered variable tick scheme name.
95    pub tick_scheme: Option<Ustr>,
96    /// Additional instrument metadata as a JSON-serializable dictionary.
97    pub info: Option<Params>,
98    /// UNIX timestamp (nanoseconds) when the data event occurred.
99    pub ts_event: UnixNanos,
100    /// UNIX timestamp (nanoseconds) when the data object was initialized.
101    pub ts_init: UnixNanos,
102}
103
104#[bon::bon]
105impl BinaryOption {
106    #[expect(clippy::too_many_arguments)]
107    fn new_checked(
108        instrument_id: InstrumentId,
109        raw_symbol: Symbol,
110        asset_class: AssetClass,
111        currency: Currency,
112        activation_ns: UnixNanos,
113        expiration_ns: UnixNanos,
114        price_precision: u8,
115        size_precision: u8,
116        price_increment: Price,
117        size_increment: Quantity,
118        outcome: Option<Ustr>,
119        description: Option<Ustr>,
120        max_quantity: Option<Quantity>,
121        min_quantity: Option<Quantity>,
122        max_notional: Option<Money>,
123        min_notional: Option<Money>,
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        tick_scheme: Option<Ustr>,
131        info: Option<Params>,
132        ts_event: UnixNanos,
133        ts_init: UnixNanos,
134    ) -> CorrectnessResult<Self> {
135        check_equal_u8(
136            price_precision,
137            price_increment.precision,
138            stringify!(price_precision),
139            stringify!(price_increment.precision),
140        )?;
141        check_equal_u8(
142            size_precision,
143            size_increment.precision,
144            stringify!(size_precision),
145            stringify!(size_increment.precision),
146        )?;
147        check_positive_price(price_increment, stringify!(price_increment))?;
148        check_positive_quantity(size_increment, stringify!(size_increment))?;
149        check_tick_scheme(tick_scheme)?;
150
151        Ok(Self {
152            id: instrument_id,
153            raw_symbol,
154            asset_class,
155            currency,
156            activation_ns,
157            expiration_ns,
158            price_precision,
159            size_precision,
160            price_increment,
161            size_increment,
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            outcome,
167            description,
168            max_quantity,
169            min_quantity,
170            max_notional,
171            min_notional,
172            max_price,
173            min_price,
174            tick_scheme,
175            info,
176            ts_event,
177            ts_init,
178        })
179    }
180
181    /// Returns a fluent builder for a [`BinaryOption`] instance.
182    ///
183    /// Required fields are enforced at compile time; optional fields can be omitted and use the
184    /// same defaults as checked construction. The same correctness checks run on `build`.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if any input validation fails.
189    #[builder(start_fn = builder, finish_fn = build)]
190    pub fn build_checked(
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        tick_scheme: Option<Ustr>,
214        info: Option<Params>,
215        ts_event: UnixNanos,
216        ts_init: UnixNanos,
217    ) -> CorrectnessResult<Self> {
218        Self::new_checked(
219            instrument_id,
220            raw_symbol,
221            asset_class,
222            currency,
223            activation_ns,
224            expiration_ns,
225            price_precision,
226            size_precision,
227            price_increment,
228            size_increment,
229            outcome,
230            description,
231            max_quantity,
232            min_quantity,
233            max_notional,
234            min_notional,
235            max_price,
236            min_price,
237            margin_init,
238            margin_maint,
239            maker_fee,
240            taker_fee,
241            tick_scheme,
242            info,
243            ts_event,
244            ts_init,
245        )
246    }
247}
248
249impl PartialEq<Self> for BinaryOption {
250    fn eq(&self, other: &Self) -> bool {
251        self.id == other.id
252    }
253}
254
255impl Eq for BinaryOption {}
256
257impl Hash for BinaryOption {
258    fn hash<H: Hasher>(&self, state: &mut H) {
259        self.id.hash(state);
260    }
261}
262
263impl Instrument for BinaryOption {
264    fn tick_scheme(&self) -> Option<Ustr> {
265        self.tick_scheme
266    }
267    fn into_any(self) -> InstrumentAny {
268        InstrumentAny::BinaryOption(self)
269    }
270
271    fn id(&self) -> InstrumentId {
272        self.id
273    }
274
275    fn raw_symbol(&self) -> Symbol {
276        self.raw_symbol
277    }
278
279    fn asset_class(&self) -> AssetClass {
280        self.asset_class
281    }
282
283    fn instrument_class(&self) -> InstrumentClass {
284        InstrumentClass::BinaryOption
285    }
286
287    fn underlying(&self) -> Option<Ustr> {
288        None
289    }
290
291    fn base_currency(&self) -> Option<Currency> {
292        None
293    }
294
295    fn quote_currency(&self) -> Currency {
296        self.currency
297    }
298
299    fn settlement_currency(&self) -> Currency {
300        self.currency
301    }
302
303    fn isin(&self) -> Option<Ustr> {
304        None
305    }
306
307    fn exchange(&self) -> Option<Ustr> {
308        None
309    }
310
311    fn option_kind(&self) -> Option<OptionKind> {
312        None
313    }
314
315    fn is_inverse(&self) -> bool {
316        false
317    }
318
319    fn price_precision(&self) -> u8 {
320        self.price_precision
321    }
322
323    fn size_precision(&self) -> u8 {
324        self.size_precision
325    }
326
327    fn price_increment(&self) -> Price {
328        self.price_increment
329    }
330
331    fn size_increment(&self) -> Quantity {
332        self.size_increment
333    }
334
335    fn multiplier(&self) -> Quantity {
336        Quantity::from(1)
337    }
338
339    fn lot_size(&self) -> Option<Quantity> {
340        Some(Quantity::from(1))
341    }
342
343    fn max_quantity(&self) -> Option<Quantity> {
344        self.max_quantity
345    }
346
347    fn min_quantity(&self) -> Option<Quantity> {
348        self.min_quantity
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    fn margin_init(&self) -> Decimal {
368        self.margin_init
369    }
370
371    fn margin_maint(&self) -> Decimal {
372        self.margin_maint
373    }
374
375    fn maker_fee(&self) -> Decimal {
376        self.maker_fee
377    }
378
379    fn taker_fee(&self) -> Decimal {
380        self.taker_fee
381    }
382
383    fn strike_price(&self) -> Option<Price> {
384        None
385    }
386
387    fn activation_ns(&self) -> Option<UnixNanos> {
388        Some(self.activation_ns)
389    }
390
391    fn expiration_ns(&self) -> Option<UnixNanos> {
392        Some(self.expiration_ns)
393    }
394
395    fn max_notional(&self) -> Option<Money> {
396        self.max_notional
397    }
398
399    fn min_notional(&self) -> Option<Money> {
400        self.min_notional
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use rstest::rstest;
407    use rust_decimal_macros::dec;
408
409    use crate::{
410        enums::{AssetClass, InstrumentClass},
411        identifiers::{InstrumentId, Symbol},
412        instruments::{BinaryOption, Instrument, stubs::*},
413        types::{Currency, Money, Price, Quantity},
414    };
415
416    #[rstest]
417    fn test_trait_accessors(binary_option: BinaryOption) {
418        assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
419        assert_eq!(
420            binary_option.instrument_class(),
421            InstrumentClass::BinaryOption
422        );
423        assert_eq!(binary_option.quote_currency(), Currency::USDC());
424        assert!(!binary_option.is_inverse());
425        assert_eq!(binary_option.price_precision(), 3);
426        assert_eq!(binary_option.size_precision(), 2);
427        assert!(binary_option.activation_ns().is_some());
428        assert!(binary_option.expiration_ns().is_some());
429    }
430
431    #[rstest]
432    fn test_new_checked_price_precision_mismatch() {
433        let result = BinaryOption::new_checked(
434            InstrumentId::from("TEST.POLYMARKET"),
435            Symbol::from("TEST"),
436            AssetClass::Alternative,
437            Currency::USDC(),
438            0.into(),
439            0.into(),
440            4, // mismatch
441            2,
442            Price::from("0.001"),
443            Quantity::from("0.01"),
444            None,
445            None,
446            None,
447            None,
448            None,
449            None,
450            None,
451            None,
452            None,
453            None,
454            None,
455            None,
456            None,
457            None,
458            0.into(),
459            0.into(),
460        );
461        assert!(result.is_err());
462    }
463
464    #[rstest]
465    fn test_serialization_roundtrip(binary_option: BinaryOption) {
466        let json = serde_json::to_string(&binary_option).unwrap();
467        let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
468        assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
469    }
470
471    #[rstest]
472    fn test_builder_matches_new_checked() {
473        let positional = BinaryOption::new_checked(
474            InstrumentId::from("TEST.POLYMARKET"),
475            Symbol::from("TEST"),
476            AssetClass::Alternative,
477            Currency::USDC(),
478            1.into(),
479            2.into(),
480            3,
481            2,
482            Price::from("0.001"),
483            Quantity::from("0.01"),
484            Some("Yes".into()),
485            Some("Will it happen?".into()),
486            Some(Quantity::from("10000.00")),
487            Some(Quantity::from("5.00")),
488            Some(Money::from("100000 USDC")),
489            Some(Money::from("10 USDC")),
490            Some(Price::from("0.999")),
491            Some(Price::from("0.001")),
492            Some(dec!(0.01)),
493            Some(dec!(0.02)),
494            Some(dec!(0.0002)),
495            Some(dec!(0.0004)),
496            None,
497            None,
498            3.into(),
499            4.into(),
500        )
501        .unwrap();
502
503        let built = BinaryOption::builder()
504            .instrument_id(InstrumentId::from("TEST.POLYMARKET"))
505            .raw_symbol(Symbol::from("TEST"))
506            .asset_class(AssetClass::Alternative)
507            .currency(Currency::USDC())
508            .activation_ns(1.into())
509            .expiration_ns(2.into())
510            .price_precision(3)
511            .size_precision(2)
512            .price_increment(Price::from("0.001"))
513            .size_increment(Quantity::from("0.01"))
514            .outcome("Yes".into())
515            .description("Will it happen?".into())
516            .max_quantity(Quantity::from("10000.00"))
517            .min_quantity(Quantity::from("5.00"))
518            .max_notional(Money::from("100000 USDC"))
519            .min_notional(Money::from("10 USDC"))
520            .max_price(Price::from("0.999"))
521            .min_price(Price::from("0.001"))
522            .margin_init(dec!(0.01))
523            .margin_maint(dec!(0.02))
524            .maker_fee(dec!(0.0002))
525            .taker_fee(dec!(0.0004))
526            .ts_event(3.into())
527            .ts_init(4.into())
528            .build()
529            .unwrap();
530
531        assert_eq!(
532            serde_json::to_value(&positional).unwrap(),
533            serde_json::to_value(&built).unwrap(),
534        );
535    }
536}