1use 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, 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#[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 pub id: InstrumentId,
52 pub raw_symbol: Symbol,
54 pub asset_class: AssetClass,
56 pub currency: Currency,
58 pub activation_ns: UnixNanos,
60 pub expiration_ns: UnixNanos,
62 pub price_precision: u8,
64 pub size_precision: u8,
66 pub price_increment: Price,
68 pub size_increment: Quantity,
70 pub margin_init: Decimal,
72 pub margin_maint: Decimal,
74 pub maker_fee: Decimal,
76 pub taker_fee: Decimal,
78 pub outcome: Option<Ustr>,
80 pub description: Option<Ustr>,
82 pub max_quantity: Option<Quantity>,
84 pub min_quantity: Option<Quantity>,
86 pub max_notional: Option<Money>,
88 pub min_notional: Option<Money>,
90 pub max_price: Option<Price>,
92 pub min_price: Option<Price>,
94 pub tick_scheme: Option<Ustr>,
96 pub info: Option<Params>,
98 pub ts_event: UnixNanos,
100 pub ts_init: UnixNanos,
102}
103
104#[bon::bon]
105impl BinaryOption {
106 #[expect(clippy::too_many_arguments)]
116 pub fn new_checked(
117 instrument_id: InstrumentId,
118 raw_symbol: Symbol,
119 asset_class: AssetClass,
120 currency: Currency,
121 activation_ns: UnixNanos,
122 expiration_ns: UnixNanos,
123 price_precision: u8,
124 size_precision: u8,
125 price_increment: Price,
126 size_increment: Quantity,
127 outcome: Option<Ustr>,
128 description: Option<Ustr>,
129 max_quantity: Option<Quantity>,
130 min_quantity: Option<Quantity>,
131 max_notional: Option<Money>,
132 min_notional: Option<Money>,
133 max_price: Option<Price>,
134 min_price: Option<Price>,
135 margin_init: Option<Decimal>,
136 margin_maint: Option<Decimal>,
137 maker_fee: Option<Decimal>,
138 taker_fee: Option<Decimal>,
139 tick_scheme: Option<Ustr>,
140 info: Option<Params>,
141 ts_event: UnixNanos,
142 ts_init: UnixNanos,
143 ) -> CorrectnessResult<Self> {
144 check_equal_u8(
145 price_precision,
146 price_increment.precision,
147 stringify!(price_precision),
148 stringify!(price_increment.precision),
149 )?;
150 check_equal_u8(
151 size_precision,
152 size_increment.precision,
153 stringify!(size_precision),
154 stringify!(size_increment.precision),
155 )?;
156 check_positive_price(price_increment, stringify!(price_increment))?;
157 check_positive_quantity(size_increment, stringify!(size_increment))?;
158 check_tick_scheme(tick_scheme)?;
159
160 Ok(Self {
161 id: instrument_id,
162 raw_symbol,
163 asset_class,
164 currency,
165 activation_ns,
166 expiration_ns,
167 price_precision,
168 size_precision,
169 price_increment,
170 size_increment,
171 margin_init: margin_init.unwrap_or_default(),
172 margin_maint: margin_maint.unwrap_or_default(),
173 maker_fee: maker_fee.unwrap_or_default(),
174 taker_fee: taker_fee.unwrap_or_default(),
175 outcome,
176 description,
177 max_quantity,
178 min_quantity,
179 max_notional,
180 min_notional,
181 max_price,
182 min_price,
183 tick_scheme,
184 info,
185 ts_event,
186 ts_init,
187 })
188 }
189
190 #[expect(clippy::too_many_arguments)]
196 #[must_use]
197 pub fn new(
198 instrument_id: InstrumentId,
199 raw_symbol: Symbol,
200 asset_class: AssetClass,
201 currency: Currency,
202 activation_ns: UnixNanos,
203 expiration_ns: UnixNanos,
204 price_precision: u8,
205 size_precision: u8,
206 price_increment: Price,
207 size_increment: Quantity,
208 outcome: Option<Ustr>,
209 description: Option<Ustr>,
210 max_quantity: Option<Quantity>,
211 min_quantity: Option<Quantity>,
212 max_notional: Option<Money>,
213 min_notional: Option<Money>,
214 max_price: Option<Price>,
215 min_price: Option<Price>,
216 margin_init: Option<Decimal>,
217 margin_maint: Option<Decimal>,
218 maker_fee: Option<Decimal>,
219 taker_fee: Option<Decimal>,
220 tick_scheme: Option<Ustr>,
221 info: Option<Params>,
222 ts_event: UnixNanos,
223 ts_init: UnixNanos,
224 ) -> Self {
225 Self::new_checked(
226 instrument_id,
227 raw_symbol,
228 asset_class,
229 currency,
230 activation_ns,
231 expiration_ns,
232 price_precision,
233 size_precision,
234 price_increment,
235 size_increment,
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 .expect_display(FAILED)
254 }
255
256 #[builder(start_fn = builder, finish_fn = build)]
266 pub fn build_checked(
267 instrument_id: InstrumentId,
268 raw_symbol: Symbol,
269 asset_class: AssetClass,
270 currency: Currency,
271 activation_ns: UnixNanos,
272 expiration_ns: UnixNanos,
273 price_precision: u8,
274 size_precision: u8,
275 price_increment: Price,
276 size_increment: Quantity,
277 outcome: Option<Ustr>,
278 description: Option<Ustr>,
279 max_quantity: Option<Quantity>,
280 min_quantity: Option<Quantity>,
281 max_notional: Option<Money>,
282 min_notional: Option<Money>,
283 max_price: Option<Price>,
284 min_price: Option<Price>,
285 margin_init: Option<Decimal>,
286 margin_maint: Option<Decimal>,
287 maker_fee: Option<Decimal>,
288 taker_fee: Option<Decimal>,
289 tick_scheme: Option<Ustr>,
290 info: Option<Params>,
291 ts_event: UnixNanos,
292 ts_init: UnixNanos,
293 ) -> CorrectnessResult<Self> {
294 Self::new_checked(
295 instrument_id,
296 raw_symbol,
297 asset_class,
298 currency,
299 activation_ns,
300 expiration_ns,
301 price_precision,
302 size_precision,
303 price_increment,
304 size_increment,
305 outcome,
306 description,
307 max_quantity,
308 min_quantity,
309 max_notional,
310 min_notional,
311 max_price,
312 min_price,
313 margin_init,
314 margin_maint,
315 maker_fee,
316 taker_fee,
317 tick_scheme,
318 info,
319 ts_event,
320 ts_init,
321 )
322 }
323}
324
325impl PartialEq<Self> for BinaryOption {
326 fn eq(&self, other: &Self) -> bool {
327 self.id == other.id
328 }
329}
330
331impl Eq for BinaryOption {}
332
333impl Hash for BinaryOption {
334 fn hash<H: Hasher>(&self, state: &mut H) {
335 self.id.hash(state);
336 }
337}
338
339impl Instrument for BinaryOption {
340 fn tick_scheme(&self) -> Option<Ustr> {
341 self.tick_scheme
342 }
343 fn into_any(self) -> InstrumentAny {
344 InstrumentAny::BinaryOption(self)
345 }
346
347 fn id(&self) -> InstrumentId {
348 self.id
349 }
350
351 fn raw_symbol(&self) -> Symbol {
352 self.raw_symbol
353 }
354
355 fn asset_class(&self) -> AssetClass {
356 self.asset_class
357 }
358
359 fn instrument_class(&self) -> InstrumentClass {
360 InstrumentClass::BinaryOption
361 }
362
363 fn underlying(&self) -> Option<Ustr> {
364 None
365 }
366
367 fn base_currency(&self) -> Option<Currency> {
368 None
369 }
370
371 fn quote_currency(&self) -> Currency {
372 self.currency
373 }
374
375 fn settlement_currency(&self) -> Currency {
376 self.currency
377 }
378
379 fn isin(&self) -> Option<Ustr> {
380 None
381 }
382
383 fn exchange(&self) -> Option<Ustr> {
384 None
385 }
386
387 fn option_kind(&self) -> Option<OptionKind> {
388 None
389 }
390
391 fn is_inverse(&self) -> bool {
392 false
393 }
394
395 fn price_precision(&self) -> u8 {
396 self.price_precision
397 }
398
399 fn size_precision(&self) -> u8 {
400 self.size_precision
401 }
402
403 fn price_increment(&self) -> Price {
404 self.price_increment
405 }
406
407 fn size_increment(&self) -> Quantity {
408 self.size_increment
409 }
410
411 fn multiplier(&self) -> Quantity {
412 Quantity::from(1)
413 }
414
415 fn lot_size(&self) -> Option<Quantity> {
416 Some(Quantity::from(1))
417 }
418
419 fn max_quantity(&self) -> Option<Quantity> {
420 self.max_quantity
421 }
422
423 fn min_quantity(&self) -> Option<Quantity> {
424 self.min_quantity
425 }
426
427 fn max_price(&self) -> Option<Price> {
428 self.max_price
429 }
430
431 fn min_price(&self) -> Option<Price> {
432 self.min_price
433 }
434
435 fn ts_event(&self) -> UnixNanos {
436 self.ts_event
437 }
438
439 fn ts_init(&self) -> UnixNanos {
440 self.ts_init
441 }
442
443 fn margin_init(&self) -> Decimal {
444 self.margin_init
445 }
446
447 fn margin_maint(&self) -> Decimal {
448 self.margin_maint
449 }
450
451 fn maker_fee(&self) -> Decimal {
452 self.maker_fee
453 }
454
455 fn taker_fee(&self) -> Decimal {
456 self.taker_fee
457 }
458
459 fn strike_price(&self) -> Option<Price> {
460 None
461 }
462
463 fn activation_ns(&self) -> Option<UnixNanos> {
464 Some(self.activation_ns)
465 }
466
467 fn expiration_ns(&self) -> Option<UnixNanos> {
468 Some(self.expiration_ns)
469 }
470
471 fn max_notional(&self) -> Option<Money> {
472 self.max_notional
473 }
474
475 fn min_notional(&self) -> Option<Money> {
476 self.min_notional
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use rstest::rstest;
483 use rust_decimal_macros::dec;
484
485 use crate::{
486 enums::{AssetClass, InstrumentClass},
487 identifiers::{InstrumentId, Symbol},
488 instruments::{BinaryOption, Instrument, stubs::*},
489 types::{Currency, Money, Price, Quantity},
490 };
491
492 #[rstest]
493 fn test_trait_accessors(binary_option: BinaryOption) {
494 assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
495 assert_eq!(
496 binary_option.instrument_class(),
497 InstrumentClass::BinaryOption
498 );
499 assert_eq!(binary_option.quote_currency(), Currency::USDC());
500 assert!(!binary_option.is_inverse());
501 assert_eq!(binary_option.price_precision(), 3);
502 assert_eq!(binary_option.size_precision(), 2);
503 assert!(binary_option.activation_ns().is_some());
504 assert!(binary_option.expiration_ns().is_some());
505 }
506
507 #[rstest]
508 fn test_new_checked_price_precision_mismatch() {
509 let result = BinaryOption::new_checked(
510 InstrumentId::from("TEST.POLYMARKET"),
511 Symbol::from("TEST"),
512 AssetClass::Alternative,
513 Currency::USDC(),
514 0.into(),
515 0.into(),
516 4, 2,
518 Price::from("0.001"),
519 Quantity::from("0.01"),
520 None,
521 None,
522 None,
523 None,
524 None,
525 None,
526 None,
527 None,
528 None,
529 None,
530 None,
531 None,
532 None,
533 None,
534 0.into(),
535 0.into(),
536 );
537 assert!(result.is_err());
538 }
539
540 #[rstest]
541 fn test_serialization_roundtrip(binary_option: BinaryOption) {
542 let json = serde_json::to_string(&binary_option).unwrap();
543 let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
544 assert_eq!(binary_option, deserialized);
545 }
546
547 #[rstest]
548 fn test_builder_matches_new_checked() {
549 let positional = BinaryOption::new_checked(
550 InstrumentId::from("TEST.POLYMARKET"),
551 Symbol::from("TEST"),
552 AssetClass::Alternative,
553 Currency::USDC(),
554 1.into(),
555 2.into(),
556 3,
557 2,
558 Price::from("0.001"),
559 Quantity::from("0.01"),
560 Some("Yes".into()),
561 Some("Will it happen?".into()),
562 Some(Quantity::from("10000.00")),
563 Some(Quantity::from("5.00")),
564 Some(Money::from("100000 USDC")),
565 Some(Money::from("10 USDC")),
566 Some(Price::from("0.999")),
567 Some(Price::from("0.001")),
568 Some(dec!(0.01)),
569 Some(dec!(0.02)),
570 Some(dec!(0.0002)),
571 Some(dec!(0.0004)),
572 None,
573 None,
574 3.into(),
575 4.into(),
576 )
577 .unwrap();
578
579 let built = BinaryOption::builder()
580 .instrument_id(InstrumentId::from("TEST.POLYMARKET"))
581 .raw_symbol(Symbol::from("TEST"))
582 .asset_class(AssetClass::Alternative)
583 .currency(Currency::USDC())
584 .activation_ns(1.into())
585 .expiration_ns(2.into())
586 .price_precision(3)
587 .size_precision(2)
588 .price_increment(Price::from("0.001"))
589 .size_increment(Quantity::from("0.01"))
590 .outcome("Yes".into())
591 .description("Will it happen?".into())
592 .max_quantity(Quantity::from("10000.00"))
593 .min_quantity(Quantity::from("5.00"))
594 .max_notional(Money::from("100000 USDC"))
595 .min_notional(Money::from("10 USDC"))
596 .max_price(Price::from("0.999"))
597 .min_price(Price::from("0.001"))
598 .margin_init(dec!(0.01))
599 .margin_maint(dec!(0.02))
600 .maker_fee(dec!(0.0002))
601 .taker_fee(dec!(0.0004))
602 .ts_event(3.into())
603 .ts_init(4.into())
604 .build()
605 .unwrap();
606
607 assert_eq!(
608 serde_json::to_value(&positional).unwrap(),
609 serde_json::to_value(&built).unwrap(),
610 );
611 }
612}