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 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)]
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 #[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, 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}