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