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 #[serde(default)]
76 pub event_id: Option<Ustr>,
77 pub outcome: Option<Ustr>,
79 pub description: Option<Ustr>,
81 pub max_quantity: Option<Quantity>,
83 pub min_quantity: Option<Quantity>,
85 pub max_notional: Option<Money>,
87 pub min_notional: Option<Money>,
89 pub max_price: Option<Price>,
91 pub min_price: Option<Price>,
93 pub tick_scheme: Option<Ustr>,
95 pub info: Option<Params>,
97 pub ts_event: UnixNanos,
99 pub ts_init: UnixNanos,
101}
102
103#[bon::bon]
104impl BinaryOption {
105 #[expect(clippy::too_many_arguments)]
106 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 event_id: Option<Ustr>,
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 tick_scheme: Option<Ustr>,
129 info: Option<Params>,
130 ts_event: UnixNanos,
131 ts_init: UnixNanos,
132 ) -> CorrectnessResult<Self> {
133 check_equal_u8(
134 price_precision,
135 price_increment.precision,
136 stringify!(price_precision),
137 stringify!(price_increment.precision),
138 )?;
139 check_equal_u8(
140 size_precision,
141 size_increment.precision,
142 stringify!(size_precision),
143 stringify!(size_increment.precision),
144 )?;
145 check_positive_price(price_increment, stringify!(price_increment))?;
146 check_positive_quantity(size_increment, stringify!(size_increment))?;
147 check_tick_scheme(tick_scheme)?;
148
149 Ok(Self {
150 id: instrument_id,
151 raw_symbol,
152 asset_class,
153 currency,
154 activation_ns,
155 expiration_ns,
156 price_precision,
157 size_precision,
158 price_increment,
159 size_increment,
160 margin_init: margin_init.unwrap_or_default(),
161 margin_maint: margin_maint.unwrap_or_default(),
162 event_id,
163 outcome,
164 description,
165 max_quantity,
166 min_quantity,
167 max_notional,
168 min_notional,
169 max_price,
170 min_price,
171 tick_scheme,
172 info,
173 ts_event,
174 ts_init,
175 })
176 }
177
178 #[builder(start_fn = builder, finish_fn = build)]
187 pub fn build_checked(
188 instrument_id: InstrumentId,
189 raw_symbol: Symbol,
190 asset_class: AssetClass,
191 currency: Currency,
192 activation_ns: UnixNanos,
193 expiration_ns: UnixNanos,
194 price_precision: u8,
195 size_precision: u8,
196 price_increment: Price,
197 size_increment: Quantity,
198 event_id: Option<Ustr>,
199 outcome: Option<Ustr>,
200 description: Option<Ustr>,
201 max_quantity: Option<Quantity>,
202 min_quantity: Option<Quantity>,
203 max_notional: Option<Money>,
204 min_notional: Option<Money>,
205 max_price: Option<Price>,
206 min_price: Option<Price>,
207 margin_init: Option<Decimal>,
208 margin_maint: Option<Decimal>,
209 tick_scheme: Option<Ustr>,
210 info: Option<Params>,
211 ts_event: UnixNanos,
212 ts_init: UnixNanos,
213 ) -> CorrectnessResult<Self> {
214 Self::new_checked(
215 instrument_id,
216 raw_symbol,
217 asset_class,
218 currency,
219 activation_ns,
220 expiration_ns,
221 price_precision,
222 size_precision,
223 price_increment,
224 size_increment,
225 event_id,
226 outcome,
227 description,
228 max_quantity,
229 min_quantity,
230 max_notional,
231 min_notional,
232 max_price,
233 min_price,
234 margin_init,
235 margin_maint,
236 tick_scheme,
237 info,
238 ts_event,
239 ts_init,
240 )
241 }
242}
243
244impl PartialEq<Self> for BinaryOption {
245 fn eq(&self, other: &Self) -> bool {
246 self.id == other.id
247 }
248}
249
250impl Eq for BinaryOption {}
251
252impl Hash for BinaryOption {
253 fn hash<H: Hasher>(&self, state: &mut H) {
254 self.id.hash(state);
255 }
256}
257
258impl Instrument for BinaryOption {
259 fn into_any(self) -> InstrumentAny {
260 InstrumentAny::BinaryOption(self)
261 }
262
263 fn id(&self) -> InstrumentId {
264 self.id
265 }
266
267 fn raw_symbol(&self) -> Symbol {
268 self.raw_symbol
269 }
270
271 fn asset_class(&self) -> AssetClass {
272 self.asset_class
273 }
274
275 fn instrument_class(&self) -> InstrumentClass {
276 InstrumentClass::BinaryOption
277 }
278
279 fn underlying(&self) -> Option<Ustr> {
280 None
281 }
282
283 fn base_currency(&self) -> Option<Currency> {
284 None
285 }
286
287 fn quote_currency(&self) -> Currency {
288 self.currency
289 }
290
291 fn settlement_currency(&self) -> Currency {
292 self.currency
293 }
294
295 fn isin(&self) -> Option<Ustr> {
296 None
297 }
298
299 fn exchange(&self) -> Option<Ustr> {
300 None
301 }
302
303 fn option_kind(&self) -> Option<OptionKind> {
304 None
305 }
306
307 fn is_inverse(&self) -> bool {
308 false
309 }
310
311 fn price_precision(&self) -> u8 {
312 self.price_precision
313 }
314
315 fn size_precision(&self) -> u8 {
316 self.size_precision
317 }
318
319 fn price_increment(&self) -> Price {
320 self.price_increment
321 }
322
323 fn size_increment(&self) -> Quantity {
324 self.size_increment
325 }
326
327 fn multiplier(&self) -> Quantity {
328 Quantity::from(1)
329 }
330
331 fn lot_size(&self) -> Option<Quantity> {
332 Some(Quantity::from(1))
333 }
334
335 fn max_quantity(&self) -> Option<Quantity> {
336 self.max_quantity
337 }
338
339 fn min_quantity(&self) -> Option<Quantity> {
340 self.min_quantity
341 }
342
343 fn max_price(&self) -> Option<Price> {
344 self.max_price
345 }
346
347 fn min_price(&self) -> Option<Price> {
348 self.min_price
349 }
350
351 fn tick_scheme(&self) -> Option<Ustr> {
352 self.tick_scheme
353 }
354
355 fn info(&self) -> Option<&Params> {
356 self.info.as_ref()
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 strike_price(&self) -> Option<Price> {
376 None
377 }
378
379 fn activation_ns(&self) -> Option<UnixNanos> {
380 Some(self.activation_ns)
381 }
382
383 fn expiration_ns(&self) -> Option<UnixNanos> {
384 Some(self.expiration_ns)
385 }
386
387 fn max_notional(&self) -> Option<Money> {
388 self.max_notional
389 }
390
391 fn min_notional(&self) -> Option<Money> {
392 self.min_notional
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use rstest::rstest;
399 use rust_decimal_macros::dec;
400
401 use crate::{
402 enums::{AssetClass, InstrumentClass},
403 identifiers::{InstrumentId, Symbol},
404 instruments::{BinaryOption, Instrument, stubs::*},
405 types::{Currency, Money, Price, Quantity},
406 };
407
408 #[rstest]
409 fn test_trait_accessors(binary_option: BinaryOption) {
410 assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
411 assert_eq!(
412 binary_option.instrument_class(),
413 InstrumentClass::BinaryOption
414 );
415 assert_eq!(binary_option.quote_currency(), Currency::USDC());
416 assert!(!binary_option.is_inverse());
417 assert_eq!(binary_option.price_precision(), 3);
418 assert_eq!(binary_option.size_precision(), 2);
419 assert!(binary_option.activation_ns().is_some());
420 assert!(binary_option.expiration_ns().is_some());
421 }
422
423 #[rstest]
424 fn test_new_checked_price_precision_mismatch() {
425 let result = BinaryOption::new_checked(
426 InstrumentId::from("TEST.POLYMARKET"),
427 Symbol::from("TEST"),
428 AssetClass::Alternative,
429 Currency::USDC(),
430 0.into(),
431 0.into(),
432 4, 2,
434 Price::from("0.001"),
435 Quantity::from("0.01"),
436 None,
437 None,
438 None,
439 None,
440 None,
441 None,
442 None,
443 None,
444 None,
445 None,
446 None,
447 None,
448 None,
449 0.into(),
450 0.into(),
451 );
452 assert!(result.is_err());
453 }
454
455 #[rstest]
456 fn test_event_id_serialization_and_legacy_default(mut binary_option: BinaryOption) {
457 binary_option.event_id = Some("event-123".into());
458 let mut value = serde_json::to_value(&binary_option).unwrap();
459 let restored: BinaryOption = serde_json::from_value(value.clone()).unwrap();
460 assert_eq!(restored.event_id, binary_option.event_id);
461 value.as_object_mut().unwrap().remove("event_id");
462 let restored: BinaryOption = serde_json::from_value(value).unwrap();
463 assert_eq!(restored.event_id, None);
464 }
465
466 #[rstest]
467 fn test_serialization_roundtrip(binary_option: BinaryOption) {
468 let json = serde_json::to_string(&binary_option).unwrap();
469 let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
470 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
471 }
472
473 #[rstest]
474 fn test_builder_matches_new_checked() {
475 let positional = BinaryOption::new_checked(
476 InstrumentId::from("TEST.POLYMARKET"),
477 Symbol::from("TEST"),
478 AssetClass::Alternative,
479 Currency::USDC(),
480 1.into(),
481 2.into(),
482 3,
483 2,
484 Price::from("0.001"),
485 Quantity::from("0.01"),
486 Some("event-123".into()),
487 Some("Yes".into()),
488 Some("Will it happen?".into()),
489 Some(Quantity::from("10000.00")),
490 Some(Quantity::from("5.00")),
491 Some(Money::from("100000 USDC")),
492 Some(Money::from("10 USDC")),
493 Some(Price::from("0.999")),
494 Some(Price::from("0.001")),
495 Some(dec!(0.01)),
496 Some(dec!(0.02)),
497 None,
498 None,
499 3.into(),
500 4.into(),
501 )
502 .unwrap();
503
504 let built = BinaryOption::builder()
505 .instrument_id(InstrumentId::from("TEST.POLYMARKET"))
506 .raw_symbol(Symbol::from("TEST"))
507 .asset_class(AssetClass::Alternative)
508 .currency(Currency::USDC())
509 .activation_ns(1.into())
510 .expiration_ns(2.into())
511 .price_precision(3)
512 .size_precision(2)
513 .price_increment(Price::from("0.001"))
514 .size_increment(Quantity::from("0.01"))
515 .event_id("event-123".into())
516 .outcome("Yes".into())
517 .description("Will it happen?".into())
518 .max_quantity(Quantity::from("10000.00"))
519 .min_quantity(Quantity::from("5.00"))
520 .max_notional(Money::from("100000 USDC"))
521 .min_notional(Money::from("10 USDC"))
522 .max_price(Price::from("0.999"))
523 .min_price(Price::from("0.001"))
524 .margin_init(dec!(0.01))
525 .margin_maint(dec!(0.02))
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}