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};
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 info: Option<Params>,
104 pub ts_event: UnixNanos,
106 pub ts_init: UnixNanos,
108}
109
110impl CryptoOption {
111 #[expect(clippy::too_many_arguments)]
120 pub fn new_checked(
121 instrument_id: InstrumentId,
122 raw_symbol: Symbol,
123 underlying: Currency,
124 quote_currency: Currency,
125 settlement_currency: Currency,
126 is_inverse: bool,
127 option_kind: OptionKind,
128 strike_price: Price,
129 activation_ns: UnixNanos,
130 expiration_ns: UnixNanos,
131 price_precision: u8,
132 size_precision: u8,
133 price_increment: Price,
134 size_increment: Quantity,
135 multiplier: Option<Quantity>,
136 lot_size: Option<Quantity>,
137 max_quantity: Option<Quantity>,
138 min_quantity: Option<Quantity>,
139 max_notional: Option<Money>,
140 min_notional: Option<Money>,
141 max_price: Option<Price>,
142 min_price: Option<Price>,
143 margin_init: Option<Decimal>,
144 margin_maint: Option<Decimal>,
145 maker_fee: Option<Decimal>,
146 taker_fee: Option<Decimal>,
147 info: Option<Params>,
148 ts_event: UnixNanos,
149 ts_init: UnixNanos,
150 ) -> CorrectnessResult<Self> {
151 check_equal_u8(
152 price_precision,
153 price_increment.precision,
154 stringify!(price_precision),
155 stringify!(price_increment.precision),
156 )?;
157 check_equal_u8(
158 size_precision,
159 size_increment.precision,
160 stringify!(size_precision),
161 stringify!(size_increment.precision),
162 )?;
163 check_positive_price(price_increment, stringify!(price_increment))?;
164 check_positive_quantity(size_increment, stringify!(size_increment))?;
165
166 if let Some(multiplier) = multiplier {
167 check_positive_quantity(multiplier, stringify!(multiplier))?;
168 }
169
170 Ok(Self {
171 id: instrument_id,
172 raw_symbol,
173 underlying,
174 quote_currency,
175 settlement_currency,
176 is_inverse,
177 option_kind,
178 strike_price,
179 activation_ns,
180 expiration_ns,
181 price_precision,
182 size_precision,
183 price_increment,
184 size_increment,
185 multiplier: multiplier.unwrap_or(Quantity::from(1)),
186 lot_size: lot_size.unwrap_or(Quantity::from(1)),
187 margin_init: margin_init.unwrap_or_default(),
188 margin_maint: margin_maint.unwrap_or_default(),
189 maker_fee: maker_fee.unwrap_or_default(),
190 taker_fee: taker_fee.unwrap_or_default(),
191 max_notional,
192 min_notional,
193 max_quantity,
194 min_quantity: Some(min_quantity.unwrap_or(1.into())),
195 max_price,
196 min_price,
197 info,
198 ts_event,
199 ts_init,
200 })
201 }
202
203 #[expect(clippy::too_many_arguments)]
209 #[must_use]
210 pub fn new(
211 instrument_id: InstrumentId,
212 raw_symbol: Symbol,
213 underlying: Currency,
214 quote_currency: Currency,
215 settlement_currency: Currency,
216 is_inverse: bool,
217 option_kind: OptionKind,
218 strike_price: Price,
219 activation_ns: UnixNanos,
220 expiration_ns: UnixNanos,
221 price_precision: u8,
222 size_precision: u8,
223 price_increment: Price,
224 size_increment: Quantity,
225 multiplier: Option<Quantity>,
226 lot_size: Option<Quantity>,
227 max_quantity: Option<Quantity>,
228 min_quantity: Option<Quantity>,
229 max_notional: Option<Money>,
230 min_notional: Option<Money>,
231 max_price: Option<Price>,
232 min_price: Option<Price>,
233 margin_init: Option<Decimal>,
234 margin_maint: Option<Decimal>,
235 maker_fee: Option<Decimal>,
236 taker_fee: Option<Decimal>,
237 info: Option<Params>,
238 ts_event: UnixNanos,
239 ts_init: UnixNanos,
240 ) -> Self {
241 Self::new_checked(
242 instrument_id,
243 raw_symbol,
244 underlying,
245 quote_currency,
246 settlement_currency,
247 is_inverse,
248 option_kind,
249 strike_price,
250 activation_ns,
251 expiration_ns,
252 price_precision,
253 size_precision,
254 price_increment,
255 size_increment,
256 multiplier,
257 lot_size,
258 max_quantity,
259 min_quantity,
260 max_notional,
261 min_notional,
262 max_price,
263 min_price,
264 margin_init,
265 margin_maint,
266 maker_fee,
267 taker_fee,
268 info,
269 ts_event,
270 ts_init,
271 )
272 .expect_display(FAILED)
273 }
274}
275
276impl PartialEq<Self> for CryptoOption {
277 fn eq(&self, other: &Self) -> bool {
278 self.id == other.id
279 }
280}
281
282impl Eq for CryptoOption {}
283
284impl Hash for CryptoOption {
285 fn hash<H: Hasher>(&self, state: &mut H) {
286 self.id.hash(state);
287 }
288}
289
290impl Instrument for CryptoOption {
291 fn into_any(self) -> InstrumentAny {
292 InstrumentAny::CryptoOption(self)
293 }
294
295 fn id(&self) -> InstrumentId {
296 self.id
297 }
298
299 fn raw_symbol(&self) -> Symbol {
300 self.raw_symbol
301 }
302
303 fn asset_class(&self) -> AssetClass {
304 AssetClass::Cryptocurrency
305 }
306
307 fn instrument_class(&self) -> InstrumentClass {
308 InstrumentClass::Option
309 }
310
311 fn underlying(&self) -> Option<Ustr> {
312 Some(self.underlying.code)
313 }
314
315 fn base_currency(&self) -> Option<Currency> {
316 Some(self.underlying)
317 }
318
319 fn quote_currency(&self) -> Currency {
320 self.quote_currency
321 }
322
323 fn settlement_currency(&self) -> Currency {
324 self.settlement_currency
325 }
326
327 fn is_inverse(&self) -> bool {
328 self.is_inverse
329 }
330
331 fn isin(&self) -> Option<Ustr> {
332 None }
334
335 fn option_kind(&self) -> Option<OptionKind> {
336 Some(self.option_kind)
337 }
338
339 fn strike_price(&self) -> Option<Price> {
340 Some(self.strike_price)
341 }
342
343 fn activation_ns(&self) -> Option<UnixNanos> {
344 Some(self.activation_ns)
345 }
346
347 fn expiration_ns(&self) -> Option<UnixNanos> {
348 Some(self.expiration_ns)
349 }
350
351 fn exchange(&self) -> Option<Ustr> {
352 None }
354
355 fn price_precision(&self) -> u8 {
356 self.price_precision
357 }
358
359 fn size_precision(&self) -> u8 {
360 self.size_precision
361 }
362
363 fn price_increment(&self) -> Price {
364 self.price_increment
365 }
366
367 fn size_increment(&self) -> Quantity {
368 self.size_increment
369 }
370
371 fn multiplier(&self) -> Quantity {
372 self.multiplier
373 }
374
375 fn lot_size(&self) -> Option<Quantity> {
376 Some(self.lot_size)
377 }
378
379 fn max_quantity(&self) -> Option<Quantity> {
380 self.max_quantity
381 }
382
383 fn min_quantity(&self) -> Option<Quantity> {
384 self.min_quantity
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 fn max_price(&self) -> Option<Price> {
396 self.max_price
397 }
398
399 fn min_price(&self) -> Option<Price> {
400 self.min_price
401 }
402
403 fn ts_event(&self) -> UnixNanos {
404 self.ts_event
405 }
406
407 fn ts_init(&self) -> UnixNanos {
408 self.ts_init
409 }
410
411 fn margin_init(&self) -> Decimal {
412 self.margin_init
413 }
414
415 fn margin_maint(&self) -> Decimal {
416 self.margin_maint
417 }
418
419 fn maker_fee(&self) -> Decimal {
420 self.maker_fee
421 }
422
423 fn taker_fee(&self) -> Decimal {
424 self.taker_fee
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use rstest::rstest;
431
432 use crate::{
433 enums::{AssetClass, InstrumentClass, OptionKind},
434 identifiers::{InstrumentId, Symbol},
435 instruments::{CryptoOption, Instrument, stubs::*},
436 types::{Currency, Price, Quantity},
437 };
438
439 #[rstest]
440 fn test_trait_accessors(crypto_option_btc_deribit: CryptoOption) {
441 assert_eq!(
442 crypto_option_btc_deribit.id(),
443 InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"),
444 );
445 assert_eq!(
446 crypto_option_btc_deribit.asset_class(),
447 AssetClass::Cryptocurrency
448 );
449 assert_eq!(
450 crypto_option_btc_deribit.instrument_class(),
451 InstrumentClass::Option
452 );
453 assert_eq!(
454 crypto_option_btc_deribit.option_kind(),
455 Some(OptionKind::Put)
456 );
457 assert_eq!(
458 crypto_option_btc_deribit.strike_price(),
459 Some(Price::from("16000.000"))
460 );
461 assert!(!crypto_option_btc_deribit.is_inverse());
462 assert_eq!(crypto_option_btc_deribit.price_precision(), 3);
463 assert_eq!(crypto_option_btc_deribit.size_precision(), 1);
464 assert_eq!(
465 crypto_option_btc_deribit.min_quantity(),
466 Some(Quantity::from("0.1"))
467 );
468 assert!(crypto_option_btc_deribit.activation_ns().is_some());
469 assert!(crypto_option_btc_deribit.expiration_ns().is_some());
470 }
471
472 #[rstest]
473 fn test_new_checked_price_precision_mismatch() {
474 let result = CryptoOption::new_checked(
475 InstrumentId::from("TEST.DERIBIT"),
476 Symbol::from("TEST"),
477 Currency::BTC(),
478 Currency::USD(),
479 Currency::BTC(),
480 false,
481 OptionKind::Call,
482 Price::from("50000.0"),
483 0.into(),
484 0.into(),
485 4, 1,
487 Price::from("0.001"),
488 Quantity::from("0.1"),
489 None,
490 None,
491 None,
492 None,
493 None,
494 None,
495 None,
496 None,
497 None,
498 None,
499 None,
500 None,
501 None,
502 0.into(),
503 0.into(),
504 );
505 assert!(result.is_err());
506 }
507
508 #[rstest]
509 fn test_serialization_roundtrip(crypto_option_btc_deribit: CryptoOption) {
510 let json = serde_json::to_string(&crypto_option_btc_deribit).unwrap();
511 let deserialized: CryptoOption = serde_json::from_str(&json).unwrap();
512 assert_eq!(crypto_option_btc_deribit, deserialized);
513 }
514}