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 CryptoOption {
50 pub id: InstrumentId,
52 pub raw_symbol: Symbol,
54 pub underlying: Currency,
56 pub quote_currency: Currency,
58 pub settlement_currency: Currency,
60 pub is_inverse: bool,
62 pub option_kind: OptionKind,
64 pub strike_price: Price,
66 pub activation_ns: UnixNanos,
68 pub expiration_ns: UnixNanos,
70 pub price_precision: u8,
72 pub size_precision: u8,
74 pub price_increment: Price,
76 pub size_increment: Quantity,
78 pub multiplier: Quantity,
80 pub lot_size: Quantity,
82 pub margin_init: Decimal,
84 pub margin_maint: Decimal,
86 pub maker_fee: Decimal,
88 pub taker_fee: Decimal,
90 pub max_quantity: Option<Quantity>,
92 pub min_quantity: Option<Quantity>,
94 pub max_notional: Option<Money>,
96 pub min_notional: Option<Money>,
98 pub max_price: Option<Price>,
100 pub min_price: Option<Price>,
102 pub tick_scheme: Option<Ustr>,
104 pub info: Option<Params>,
106 pub ts_event: UnixNanos,
108 pub ts_init: UnixNanos,
110}
111
112#[bon::bon]
113impl CryptoOption {
114 #[expect(clippy::too_many_arguments)]
124 pub fn new_checked(
125 instrument_id: InstrumentId,
126 raw_symbol: Symbol,
127 underlying: Currency,
128 quote_currency: Currency,
129 settlement_currency: Currency,
130 is_inverse: bool,
131 option_kind: OptionKind,
132 strike_price: Price,
133 activation_ns: UnixNanos,
134 expiration_ns: UnixNanos,
135 price_precision: u8,
136 size_precision: u8,
137 price_increment: Price,
138 size_increment: Quantity,
139 multiplier: Option<Quantity>,
140 lot_size: Option<Quantity>,
141 max_quantity: Option<Quantity>,
142 min_quantity: Option<Quantity>,
143 max_notional: Option<Money>,
144 min_notional: Option<Money>,
145 max_price: Option<Price>,
146 min_price: Option<Price>,
147 margin_init: Option<Decimal>,
148 margin_maint: Option<Decimal>,
149 maker_fee: Option<Decimal>,
150 taker_fee: Option<Decimal>,
151 tick_scheme: Option<Ustr>,
152 info: Option<Params>,
153 ts_event: UnixNanos,
154 ts_init: UnixNanos,
155 ) -> CorrectnessResult<Self> {
156 check_equal_u8(
157 price_precision,
158 price_increment.precision,
159 stringify!(price_precision),
160 stringify!(price_increment.precision),
161 )?;
162 check_equal_u8(
163 size_precision,
164 size_increment.precision,
165 stringify!(size_precision),
166 stringify!(size_increment.precision),
167 )?;
168 check_positive_price(price_increment, stringify!(price_increment))?;
169 check_positive_quantity(size_increment, stringify!(size_increment))?;
170 check_tick_scheme(tick_scheme)?;
171
172 if let Some(multiplier) = multiplier {
173 check_positive_quantity(multiplier, stringify!(multiplier))?;
174 }
175
176 if let Some(lot_size) = lot_size {
177 check_positive_quantity(lot_size, stringify!(lot_size))?;
178 }
179
180 Ok(Self {
181 id: instrument_id,
182 raw_symbol,
183 underlying,
184 quote_currency,
185 settlement_currency,
186 is_inverse,
187 option_kind,
188 strike_price,
189 activation_ns,
190 expiration_ns,
191 price_precision,
192 size_precision,
193 price_increment,
194 size_increment,
195 multiplier: multiplier.unwrap_or(Quantity::from(1)),
196 lot_size: lot_size.unwrap_or(Quantity::from(1)),
197 margin_init: margin_init.unwrap_or_default(),
198 margin_maint: margin_maint.unwrap_or_default(),
199 maker_fee: maker_fee.unwrap_or_default(),
200 taker_fee: taker_fee.unwrap_or_default(),
201 max_notional,
202 min_notional,
203 max_quantity,
204 min_quantity: Some(min_quantity.unwrap_or(1.into())),
205 max_price,
206 min_price,
207 tick_scheme,
208 info,
209 ts_event,
210 ts_init,
211 })
212 }
213
214 #[expect(clippy::too_many_arguments)]
220 #[must_use]
221 pub fn new(
222 instrument_id: InstrumentId,
223 raw_symbol: Symbol,
224 underlying: Currency,
225 quote_currency: Currency,
226 settlement_currency: Currency,
227 is_inverse: bool,
228 option_kind: OptionKind,
229 strike_price: Price,
230 activation_ns: UnixNanos,
231 expiration_ns: UnixNanos,
232 price_precision: u8,
233 size_precision: u8,
234 price_increment: Price,
235 size_increment: Quantity,
236 multiplier: Option<Quantity>,
237 lot_size: Option<Quantity>,
238 max_quantity: Option<Quantity>,
239 min_quantity: Option<Quantity>,
240 max_notional: Option<Money>,
241 min_notional: Option<Money>,
242 max_price: Option<Price>,
243 min_price: Option<Price>,
244 margin_init: Option<Decimal>,
245 margin_maint: Option<Decimal>,
246 maker_fee: Option<Decimal>,
247 taker_fee: Option<Decimal>,
248 tick_scheme: Option<Ustr>,
249 info: Option<Params>,
250 ts_event: UnixNanos,
251 ts_init: UnixNanos,
252 ) -> Self {
253 Self::new_checked(
254 instrument_id,
255 raw_symbol,
256 underlying,
257 quote_currency,
258 settlement_currency,
259 is_inverse,
260 option_kind,
261 strike_price,
262 activation_ns,
263 expiration_ns,
264 price_precision,
265 size_precision,
266 price_increment,
267 size_increment,
268 multiplier,
269 lot_size,
270 max_quantity,
271 min_quantity,
272 max_notional,
273 min_notional,
274 max_price,
275 min_price,
276 margin_init,
277 margin_maint,
278 maker_fee,
279 taker_fee,
280 tick_scheme,
281 info,
282 ts_event,
283 ts_init,
284 )
285 .expect_display(FAILED)
286 }
287
288 #[builder(start_fn = builder, finish_fn = build)]
298 pub fn build_checked(
299 instrument_id: InstrumentId,
300 raw_symbol: Symbol,
301 underlying: Currency,
302 quote_currency: Currency,
303 settlement_currency: Currency,
304 is_inverse: bool,
305 option_kind: OptionKind,
306 strike_price: Price,
307 activation_ns: UnixNanos,
308 expiration_ns: UnixNanos,
309 price_precision: u8,
310 size_precision: u8,
311 price_increment: Price,
312 size_increment: Quantity,
313 multiplier: Option<Quantity>,
314 lot_size: Option<Quantity>,
315 max_quantity: Option<Quantity>,
316 min_quantity: Option<Quantity>,
317 max_notional: Option<Money>,
318 min_notional: Option<Money>,
319 max_price: Option<Price>,
320 min_price: Option<Price>,
321 margin_init: Option<Decimal>,
322 margin_maint: Option<Decimal>,
323 maker_fee: Option<Decimal>,
324 taker_fee: Option<Decimal>,
325 tick_scheme: Option<Ustr>,
326 info: Option<Params>,
327 ts_event: UnixNanos,
328 ts_init: UnixNanos,
329 ) -> CorrectnessResult<Self> {
330 Self::new_checked(
331 instrument_id,
332 raw_symbol,
333 underlying,
334 quote_currency,
335 settlement_currency,
336 is_inverse,
337 option_kind,
338 strike_price,
339 activation_ns,
340 expiration_ns,
341 price_precision,
342 size_precision,
343 price_increment,
344 size_increment,
345 multiplier,
346 lot_size,
347 max_quantity,
348 min_quantity,
349 max_notional,
350 min_notional,
351 max_price,
352 min_price,
353 margin_init,
354 margin_maint,
355 maker_fee,
356 taker_fee,
357 tick_scheme,
358 info,
359 ts_event,
360 ts_init,
361 )
362 }
363}
364
365impl PartialEq<Self> for CryptoOption {
366 fn eq(&self, other: &Self) -> bool {
367 self.id == other.id
368 }
369}
370
371impl Eq for CryptoOption {}
372
373impl Hash for CryptoOption {
374 fn hash<H: Hasher>(&self, state: &mut H) {
375 self.id.hash(state);
376 }
377}
378
379impl Instrument for CryptoOption {
380 fn tick_scheme(&self) -> Option<Ustr> {
381 self.tick_scheme
382 }
383 fn into_any(self) -> InstrumentAny {
384 InstrumentAny::CryptoOption(self)
385 }
386
387 fn id(&self) -> InstrumentId {
388 self.id
389 }
390
391 fn raw_symbol(&self) -> Symbol {
392 self.raw_symbol
393 }
394
395 fn asset_class(&self) -> AssetClass {
396 AssetClass::Cryptocurrency
397 }
398
399 fn instrument_class(&self) -> InstrumentClass {
400 InstrumentClass::Option
401 }
402
403 fn underlying(&self) -> Option<Ustr> {
404 Some(self.underlying.code)
405 }
406
407 fn base_currency(&self) -> Option<Currency> {
408 Some(self.underlying)
409 }
410
411 fn quote_currency(&self) -> Currency {
412 self.quote_currency
413 }
414
415 fn settlement_currency(&self) -> Currency {
416 self.settlement_currency
417 }
418
419 fn is_inverse(&self) -> bool {
420 self.is_inverse
421 }
422
423 fn isin(&self) -> Option<Ustr> {
424 None }
426
427 fn option_kind(&self) -> Option<OptionKind> {
428 Some(self.option_kind)
429 }
430
431 fn strike_price(&self) -> Option<Price> {
432 Some(self.strike_price)
433 }
434
435 fn activation_ns(&self) -> Option<UnixNanos> {
436 Some(self.activation_ns)
437 }
438
439 fn expiration_ns(&self) -> Option<UnixNanos> {
440 Some(self.expiration_ns)
441 }
442
443 fn exchange(&self) -> Option<Ustr> {
444 None }
446
447 fn price_precision(&self) -> u8 {
448 self.price_precision
449 }
450
451 fn size_precision(&self) -> u8 {
452 self.size_precision
453 }
454
455 fn price_increment(&self) -> Price {
456 self.price_increment
457 }
458
459 fn size_increment(&self) -> Quantity {
460 self.size_increment
461 }
462
463 fn multiplier(&self) -> Quantity {
464 self.multiplier
465 }
466
467 fn lot_size(&self) -> Option<Quantity> {
468 Some(self.lot_size)
469 }
470
471 fn max_quantity(&self) -> Option<Quantity> {
472 self.max_quantity
473 }
474
475 fn min_quantity(&self) -> Option<Quantity> {
476 self.min_quantity
477 }
478
479 fn max_notional(&self) -> Option<Money> {
480 self.max_notional
481 }
482
483 fn min_notional(&self) -> Option<Money> {
484 self.min_notional
485 }
486
487 fn max_price(&self) -> Option<Price> {
488 self.max_price
489 }
490
491 fn min_price(&self) -> Option<Price> {
492 self.min_price
493 }
494
495 fn ts_event(&self) -> UnixNanos {
496 self.ts_event
497 }
498
499 fn ts_init(&self) -> UnixNanos {
500 self.ts_init
501 }
502
503 fn margin_init(&self) -> Decimal {
504 self.margin_init
505 }
506
507 fn margin_maint(&self) -> Decimal {
508 self.margin_maint
509 }
510
511 fn maker_fee(&self) -> Decimal {
512 self.maker_fee
513 }
514
515 fn taker_fee(&self) -> Decimal {
516 self.taker_fee
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use rstest::rstest;
523 use rust_decimal_macros::dec;
524
525 use crate::{
526 enums::{AssetClass, InstrumentClass, OptionKind},
527 identifiers::{InstrumentId, Symbol},
528 instruments::{CryptoOption, Instrument, stubs::*},
529 types::{Currency, Money, Price, Quantity},
530 };
531
532 #[rstest]
533 fn test_trait_accessors(crypto_option_btc_deribit: CryptoOption) {
534 assert_eq!(
535 crypto_option_btc_deribit.id(),
536 InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"),
537 );
538 assert_eq!(
539 crypto_option_btc_deribit.asset_class(),
540 AssetClass::Cryptocurrency
541 );
542 assert_eq!(
543 crypto_option_btc_deribit.instrument_class(),
544 InstrumentClass::Option
545 );
546 assert_eq!(
547 crypto_option_btc_deribit.option_kind(),
548 Some(OptionKind::Put)
549 );
550 assert_eq!(
551 crypto_option_btc_deribit.strike_price(),
552 Some(Price::from("16000.000"))
553 );
554 assert!(!crypto_option_btc_deribit.is_inverse());
555 assert_eq!(crypto_option_btc_deribit.price_precision(), 3);
556 assert_eq!(crypto_option_btc_deribit.size_precision(), 1);
557 assert_eq!(
558 crypto_option_btc_deribit.min_quantity(),
559 Some(Quantity::from("0.1"))
560 );
561 assert!(crypto_option_btc_deribit.activation_ns().is_some());
562 assert!(crypto_option_btc_deribit.expiration_ns().is_some());
563 }
564
565 #[rstest]
566 fn test_new_checked_price_precision_mismatch() {
567 let result = CryptoOption::new_checked(
568 InstrumentId::from("TEST.DERIBIT"),
569 Symbol::from("TEST"),
570 Currency::BTC(),
571 Currency::USD(),
572 Currency::BTC(),
573 false,
574 OptionKind::Call,
575 Price::from("50000.0"),
576 0.into(),
577 0.into(),
578 4, 1,
580 Price::from("0.001"),
581 Quantity::from("0.1"),
582 None,
583 None,
584 None,
585 None,
586 None,
587 None,
588 None,
589 None,
590 None,
591 None,
592 None,
593 None,
594 None,
595 None,
596 0.into(),
597 0.into(),
598 );
599 assert!(result.is_err());
600 }
601
602 #[rstest]
603 fn test_new_checked_rejects_non_positive_lot_size() {
604 let result = CryptoOption::new_checked(
605 InstrumentId::from("TEST.DERIBIT"),
606 Symbol::from("TEST"),
607 Currency::BTC(),
608 Currency::USD(),
609 Currency::BTC(),
610 false,
611 OptionKind::Call,
612 Price::from("50000.0"),
613 0.into(),
614 0.into(),
615 1,
616 1,
617 Price::from("0.1"),
618 Quantity::from("0.1"),
619 None,
620 Some(Quantity::from("0")),
621 None,
622 None,
623 None,
624 None,
625 None,
626 None,
627 None,
628 None,
629 None,
630 None,
631 None,
632 None,
633 0.into(),
634 0.into(),
635 );
636 assert!(result.is_err());
637 }
638
639 #[rstest]
640 fn test_serialization_roundtrip(crypto_option_btc_deribit: CryptoOption) {
641 let json = serde_json::to_string(&crypto_option_btc_deribit).unwrap();
642 let deserialized: CryptoOption = serde_json::from_str(&json).unwrap();
643 assert_eq!(crypto_option_btc_deribit, deserialized);
644 }
645
646 #[rstest]
647 fn test_builder_matches_new_checked() {
648 let positional = CryptoOption::new_checked(
649 InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"),
650 Symbol::from("BTC-13JAN23-16000-P"),
651 Currency::BTC(),
652 Currency::USDC(),
653 Currency::USDT(),
654 false,
655 OptionKind::Put,
656 Price::from("16000.000"),
657 1.into(),
658 2.into(),
659 3,
660 1,
661 Price::from("0.001"),
662 Quantity::from("0.1"),
663 Some(Quantity::from("10")),
664 Some(Quantity::from("5")),
665 Some(Quantity::from("1000.0")),
666 Some(Quantity::from("0.1")),
667 Some(Money::new(1_000_000.0, Currency::USDC())),
668 Some(Money::new(10.0, Currency::USDC())),
669 Some(Price::from("99999.999")),
670 Some(Price::from("0.001")),
671 Some(dec!(0.01)),
672 Some(dec!(0.02)),
673 Some(dec!(0.0002)),
674 Some(dec!(0.0004)),
675 None,
676 None,
677 1.into(),
678 2.into(),
679 )
680 .unwrap();
681
682 let built = CryptoOption::builder()
683 .instrument_id(InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"))
684 .raw_symbol(Symbol::from("BTC-13JAN23-16000-P"))
685 .underlying(Currency::BTC())
686 .quote_currency(Currency::USDC())
687 .settlement_currency(Currency::USDT())
688 .is_inverse(false)
689 .option_kind(OptionKind::Put)
690 .strike_price(Price::from("16000.000"))
691 .activation_ns(1.into())
692 .expiration_ns(2.into())
693 .price_precision(3)
694 .size_precision(1)
695 .price_increment(Price::from("0.001"))
696 .size_increment(Quantity::from("0.1"))
697 .multiplier(Quantity::from("10"))
698 .lot_size(Quantity::from("5"))
699 .max_quantity(Quantity::from("1000.0"))
700 .min_quantity(Quantity::from("0.1"))
701 .max_notional(Money::new(1_000_000.0, Currency::USDC()))
702 .min_notional(Money::new(10.0, Currency::USDC()))
703 .max_price(Price::from("99999.999"))
704 .min_price(Price::from("0.001"))
705 .margin_init(dec!(0.01))
706 .margin_maint(dec!(0.02))
707 .maker_fee(dec!(0.0002))
708 .taker_fee(dec!(0.0004))
709 .ts_event(1.into())
710 .ts_init(2.into())
711 .build()
712 .unwrap();
713
714 assert_eq!(
715 serde_json::to_value(&positional).unwrap(),
716 serde_json::to_value(&built).unwrap(),
717 );
718 }
719}