1use 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#[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 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 #[serde(default)]
80 pub event_id: Option<Ustr>,
81 pub outcome: Option<Ustr>,
83 pub description: Option<Ustr>,
85 pub max_quantity: Option<Quantity>,
87 pub min_quantity: Option<Quantity>,
89 pub max_notional: Option<Money>,
91 pub min_notional: Option<Money>,
93 pub max_price: Option<Price>,
95 pub min_price: Option<Price>,
97 pub tick_scheme: Option<Ustr>,
99 pub info: Option<Params>,
101 pub ts_event: UnixNanos,
103 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 #[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, 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}