nautilus_model/instruments/
binary_option.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},
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, Copy, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.model")
44)]
45pub struct BinaryOption {
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 binary option asset class.
51    pub asset_class: AssetClass,
52    /// The binary option contract currency.
53    pub currency: Currency,
54    /// UNIX timestamp (nanoseconds) for contract activation.
55    pub activation_ns: UnixNanos,
56    /// UNIX timestamp (nanoseconds) for contract expiration.
57    pub expiration_ns: UnixNanos,
58    /// The price decimal precision.
59    pub price_precision: u8,
60    /// The trading size decimal precision.
61    pub size_precision: u8,
62    /// The minimum price increment (tick size).
63    pub price_increment: Price,
64    /// The minimum size increment.
65    pub size_increment: Quantity,
66    /// The initial (order) margin requirement in percentage of order value.
67    pub margin_init: Decimal,
68    /// The maintenance (position) margin in percentage of position value.
69    pub margin_maint: Decimal,
70    /// The fee rate for liquidity makers as a percentage of order value.
71    pub maker_fee: Decimal,
72    /// The fee rate for liquidity takers as a percentage of order value.
73    pub taker_fee: Decimal,
74    /// The binary outcome of the market.
75    pub outcome: Option<Ustr>,
76    /// The market description.
77    pub description: Option<Ustr>,
78    /// The maximum allowable order quantity.
79    pub max_quantity: Option<Quantity>,
80    /// The minimum allowable order quantity.
81    pub min_quantity: Option<Quantity>,
82    /// The maximum allowable order notional value.
83    pub max_notional: Option<Money>,
84    /// The minimum allowable order notional value.
85    pub min_notional: Option<Money>,
86    /// The maximum allowable quoted price.
87    pub max_price: Option<Price>,
88    /// The minimum allowable quoted price.
89    pub min_price: Option<Price>,
90    /// UNIX timestamp (nanoseconds) when the data event occurred.
91    pub ts_event: UnixNanos,
92    /// UNIX timestamp (nanoseconds) when the data object was initialized.
93    pub ts_init: UnixNanos,
94}
95
96impl BinaryOption {
97    /// Creates a new [`BinaryOption`] instance with correctness checking.
98    ///
99    /// # Notes
100    ///
101    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
102    /// # Errors
103    ///
104    /// Returns an error if any input validation fails (e.g., invalid precision or increments).
105    #[allow(clippy::too_many_arguments)]
106    pub fn new_checked(
107        instrument_id: InstrumentId,
108        raw_symbol: Symbol,
109        asset_class: AssetClass,
110        currency: Currency,
111        activation_ns: UnixNanos,
112        expiration_ns: UnixNanos,
113        price_precision: u8,
114        size_precision: u8,
115        price_increment: Price,
116        size_increment: Quantity,
117        outcome: Option<Ustr>,
118        description: Option<Ustr>,
119        max_quantity: Option<Quantity>,
120        min_quantity: Option<Quantity>,
121        max_notional: Option<Money>,
122        min_notional: Option<Money>,
123        max_price: Option<Price>,
124        min_price: Option<Price>,
125        margin_init: Option<Decimal>,
126        margin_maint: Option<Decimal>,
127        maker_fee: Option<Decimal>,
128        taker_fee: Option<Decimal>,
129        ts_event: UnixNanos,
130        ts_init: UnixNanos,
131    ) -> anyhow::Result<Self> {
132        check_equal_u8(
133            price_precision,
134            price_increment.precision,
135            stringify!(price_precision),
136            stringify!(price_increment.precision),
137        )?;
138        check_equal_u8(
139            size_precision,
140            size_increment.precision,
141            stringify!(size_precision),
142            stringify!(size_increment.precision),
143        )?;
144        check_positive_price(price_increment, stringify!(price_increment))?;
145        check_positive_quantity(size_increment, stringify!(size_increment))?;
146
147        Ok(Self {
148            id: instrument_id,
149            raw_symbol,
150            asset_class,
151            currency,
152            activation_ns,
153            expiration_ns,
154            price_precision,
155            size_precision,
156            price_increment,
157            size_increment,
158            margin_init: margin_init.unwrap_or_default(),
159            margin_maint: margin_maint.unwrap_or_default(),
160            maker_fee: maker_fee.unwrap_or_default(),
161            taker_fee: taker_fee.unwrap_or_default(),
162            outcome,
163            description,
164            max_quantity,
165            min_quantity,
166            max_notional,
167            min_notional,
168            max_price,
169            min_price,
170            ts_event,
171            ts_init,
172        })
173    }
174
175    /// Creates a new [`BinaryOption`] instance by validating parameters.
176    ///
177    /// # Panics
178    ///
179    /// Panics if parameter validation fails during `new_checked`.
180    #[allow(clippy::too_many_arguments)]
181    pub fn new(
182        instrument_id: InstrumentId,
183        raw_symbol: Symbol,
184        asset_class: AssetClass,
185        currency: Currency,
186        activation_ns: UnixNanos,
187        expiration_ns: UnixNanos,
188        price_precision: u8,
189        size_precision: u8,
190        price_increment: Price,
191        size_increment: Quantity,
192        outcome: Option<Ustr>,
193        description: Option<Ustr>,
194        max_quantity: Option<Quantity>,
195        min_quantity: Option<Quantity>,
196        max_notional: Option<Money>,
197        min_notional: Option<Money>,
198        max_price: Option<Price>,
199        min_price: Option<Price>,
200        margin_init: Option<Decimal>,
201        margin_maint: Option<Decimal>,
202        maker_fee: Option<Decimal>,
203        taker_fee: Option<Decimal>,
204        ts_event: UnixNanos,
205        ts_init: UnixNanos,
206    ) -> Self {
207        Self::new_checked(
208            instrument_id,
209            raw_symbol,
210            asset_class,
211            currency,
212            activation_ns,
213            expiration_ns,
214            price_precision,
215            size_precision,
216            price_increment,
217            size_increment,
218            outcome,
219            description,
220            max_quantity,
221            min_quantity,
222            max_notional,
223            min_notional,
224            max_price,
225            min_price,
226            margin_init,
227            margin_maint,
228            maker_fee,
229            taker_fee,
230            ts_event,
231            ts_init,
232        )
233        .expect(FAILED)
234    }
235}
236
237impl PartialEq<Self> for BinaryOption {
238    fn eq(&self, other: &Self) -> bool {
239        self.id == other.id
240    }
241}
242
243impl Eq for BinaryOption {}
244
245impl Hash for BinaryOption {
246    fn hash<H: Hasher>(&self, state: &mut H) {
247        self.id.hash(state);
248    }
249}
250
251impl Instrument for BinaryOption {
252    fn into_any(self) -> InstrumentAny {
253        InstrumentAny::BinaryOption(self)
254    }
255
256    fn id(&self) -> InstrumentId {
257        self.id
258    }
259
260    fn raw_symbol(&self) -> Symbol {
261        self.raw_symbol
262    }
263
264    fn asset_class(&self) -> AssetClass {
265        self.asset_class
266    }
267
268    fn instrument_class(&self) -> InstrumentClass {
269        InstrumentClass::BinaryOption
270    }
271
272    fn underlying(&self) -> Option<Ustr> {
273        None
274    }
275
276    fn base_currency(&self) -> Option<Currency> {
277        None
278    }
279
280    fn quote_currency(&self) -> Currency {
281        self.currency
282    }
283
284    fn settlement_currency(&self) -> Currency {
285        self.currency
286    }
287
288    fn isin(&self) -> Option<Ustr> {
289        None
290    }
291
292    fn exchange(&self) -> Option<Ustr> {
293        None
294    }
295
296    fn option_kind(&self) -> Option<OptionKind> {
297        None
298    }
299
300    fn is_inverse(&self) -> bool {
301        false
302    }
303
304    fn price_precision(&self) -> u8 {
305        self.price_precision
306    }
307
308    fn size_precision(&self) -> u8 {
309        self.size_precision
310    }
311
312    fn price_increment(&self) -> Price {
313        self.price_increment
314    }
315
316    fn size_increment(&self) -> Quantity {
317        self.size_increment
318    }
319
320    fn multiplier(&self) -> Quantity {
321        Quantity::from(1)
322    }
323
324    fn lot_size(&self) -> Option<Quantity> {
325        Some(Quantity::from(1))
326    }
327
328    fn max_quantity(&self) -> Option<Quantity> {
329        self.max_quantity
330    }
331
332    fn min_quantity(&self) -> Option<Quantity> {
333        self.min_quantity
334    }
335
336    fn max_price(&self) -> Option<Price> {
337        self.max_price
338    }
339
340    fn min_price(&self) -> Option<Price> {
341        self.min_price
342    }
343
344    fn ts_event(&self) -> UnixNanos {
345        self.ts_event
346    }
347
348    fn ts_init(&self) -> UnixNanos {
349        self.ts_init
350    }
351
352    fn strike_price(&self) -> Option<Price> {
353        None
354    }
355
356    fn activation_ns(&self) -> Option<UnixNanos> {
357        Some(self.activation_ns)
358    }
359
360    fn expiration_ns(&self) -> Option<UnixNanos> {
361        Some(self.expiration_ns)
362    }
363
364    fn max_notional(&self) -> Option<Money> {
365        self.max_notional
366    }
367
368    fn min_notional(&self) -> Option<Money> {
369        self.min_notional
370    }
371}
372
373////////////////////////////////////////////////////////////////////////////////
374// Tests
375////////////////////////////////////////////////////////////////////////////////
376#[cfg(test)]
377mod tests {
378    use rstest::rstest;
379
380    use crate::instruments::{BinaryOption, stubs::*};
381
382    #[rstest]
383    fn test_equality(binary_option: BinaryOption) {
384        let cloned = binary_option;
385        assert_eq!(binary_option, cloned);
386    }
387}