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 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)]
111 fn new_checked(
112 instrument_id: InstrumentId,
113 raw_symbol: Symbol,
114 underlying: Currency,
115 quote_currency: Currency,
116 settlement_currency: Currency,
117 is_inverse: bool,
118 activation_ns: UnixNanos,
119 expiration_ns: UnixNanos,
120 price_precision: u8,
121 size_precision: u8,
122 price_increment: Price,
123 size_increment: Quantity,
124 multiplier: Option<Quantity>,
125 lot_size: Option<Quantity>,
126 max_quantity: Option<Quantity>,
127 min_quantity: Option<Quantity>,
128 max_notional: Option<Money>,
129 min_notional: Option<Money>,
130 max_price: Option<Price>,
131 min_price: Option<Price>,
132 margin_init: Option<Decimal>,
133 margin_maint: Option<Decimal>,
134 maker_fee: Option<Decimal>,
135 taker_fee: Option<Decimal>,
136 tick_scheme: Option<Ustr>,
137 info: Option<Params>,
138 ts_event: UnixNanos,
139 ts_init: UnixNanos,
140 ) -> CorrectnessResult<Self> {
141 check_equal_u8(
142 price_precision,
143 price_increment.precision,
144 stringify!(price_precision),
145 stringify!(price_increment.precision),
146 )?;
147 check_equal_u8(
148 size_precision,
149 size_increment.precision,
150 stringify!(size_precision),
151 stringify!(size_increment.precision),
152 )?;
153 check_positive_price(price_increment, stringify!(price_increment))?;
154 check_positive_quantity(size_increment, stringify!(size_increment))?;
155 check_tick_scheme(tick_scheme)?;
156
157 if let Some(multiplier) = multiplier {
158 check_positive_quantity(multiplier, stringify!(multiplier))?;
159 }
160
161 if let Some(lot_size) = lot_size {
162 check_positive_quantity(lot_size, stringify!(lot_size))?;
163 }
164
165 Ok(Self {
166 id: instrument_id,
167 raw_symbol,
168 underlying,
169 quote_currency,
170 settlement_currency,
171 is_inverse,
172 activation_ns,
173 expiration_ns,
174 price_precision,
175 size_precision,
176 price_increment,
177 size_increment,
178 multiplier: multiplier.unwrap_or(Quantity::from(1)),
179 lot_size: lot_size.unwrap_or(Quantity::from(1)),
180 margin_init: margin_init.unwrap_or_default(),
181 margin_maint: margin_maint.unwrap_or_default(),
182 maker_fee: maker_fee.unwrap_or_default(),
183 taker_fee: taker_fee.unwrap_or_default(),
184 max_quantity,
185 min_quantity,
186 max_notional,
187 min_notional,
188 max_price,
189 min_price,
190 tick_scheme,
191 info,
192 ts_event,
193 ts_init,
194 })
195 }
196
197 #[builder(start_fn = builder, finish_fn = build)]
206 pub fn build_checked(
207 instrument_id: InstrumentId,
208 raw_symbol: Symbol,
209 underlying: Currency,
210 quote_currency: Currency,
211 settlement_currency: Currency,
212 is_inverse: bool,
213 activation_ns: UnixNanos,
214 expiration_ns: UnixNanos,
215 price_precision: u8,
216 size_precision: u8,
217 price_increment: Price,
218 size_increment: Quantity,
219 multiplier: Option<Quantity>,
220 lot_size: Option<Quantity>,
221 max_quantity: Option<Quantity>,
222 min_quantity: Option<Quantity>,
223 max_notional: Option<Money>,
224 min_notional: Option<Money>,
225 max_price: Option<Price>,
226 min_price: Option<Price>,
227 margin_init: Option<Decimal>,
228 margin_maint: Option<Decimal>,
229 maker_fee: Option<Decimal>,
230 taker_fee: Option<Decimal>,
231 tick_scheme: Option<Ustr>,
232 info: Option<Params>,
233 ts_event: UnixNanos,
234 ts_init: UnixNanos,
235 ) -> CorrectnessResult<Self> {
236 Self::new_checked(
237 instrument_id,
238 raw_symbol,
239 underlying,
240 quote_currency,
241 settlement_currency,
242 is_inverse,
243 activation_ns,
244 expiration_ns,
245 price_precision,
246 size_precision,
247 price_increment,
248 size_increment,
249 multiplier,
250 lot_size,
251 max_quantity,
252 min_quantity,
253 max_notional,
254 min_notional,
255 max_price,
256 min_price,
257 margin_init,
258 margin_maint,
259 maker_fee,
260 taker_fee,
261 tick_scheme,
262 info,
263 ts_event,
264 ts_init,
265 )
266 }
267}
268
269impl PartialEq<Self> for CryptoFuture {
270 fn eq(&self, other: &Self) -> bool {
271 self.id == other.id
272 }
273}
274
275impl Eq for CryptoFuture {}
276
277impl Hash for CryptoFuture {
278 fn hash<H: Hasher>(&self, state: &mut H) {
279 self.id.hash(state);
280 }
281}
282
283impl Instrument for CryptoFuture {
284 fn tick_scheme(&self) -> Option<Ustr> {
285 self.tick_scheme
286 }
287 fn into_any(self) -> InstrumentAny {
288 InstrumentAny::CryptoFuture(self)
289 }
290
291 fn id(&self) -> InstrumentId {
292 self.id
293 }
294
295 fn raw_symbol(&self) -> Symbol {
296 self.raw_symbol
297 }
298
299 fn asset_class(&self) -> AssetClass {
300 AssetClass::Cryptocurrency
301 }
302
303 fn instrument_class(&self) -> InstrumentClass {
304 InstrumentClass::Future
305 }
306
307 fn underlying(&self) -> Option<Ustr> {
308 Some(self.underlying.code)
309 }
310
311 fn base_currency(&self) -> Option<Currency> {
312 Some(self.underlying)
313 }
314
315 fn quote_currency(&self) -> Currency {
316 self.quote_currency
317 }
318
319 fn settlement_currency(&self) -> Currency {
320 self.settlement_currency
321 }
322
323 fn isin(&self) -> Option<Ustr> {
324 None
325 }
326
327 fn exchange(&self) -> Option<Ustr> {
328 None
329 }
330
331 fn option_kind(&self) -> Option<OptionKind> {
332 None
333 }
334
335 fn is_inverse(&self) -> bool {
336 self.is_inverse
337 }
338
339 fn price_precision(&self) -> u8 {
340 self.price_precision
341 }
342
343 fn size_precision(&self) -> u8 {
344 self.size_precision
345 }
346
347 fn price_increment(&self) -> Price {
348 self.price_increment
349 }
350
351 fn size_increment(&self) -> Quantity {
352 self.size_increment
353 }
354
355 fn multiplier(&self) -> Quantity {
356 self.multiplier
357 }
358
359 fn lot_size(&self) -> Option<Quantity> {
360 Some(self.lot_size)
361 }
362
363 fn max_quantity(&self) -> Option<Quantity> {
364 self.max_quantity
365 }
366
367 fn min_quantity(&self) -> Option<Quantity> {
368 self.min_quantity
369 }
370
371 fn max_price(&self) -> Option<Price> {
372 self.max_price
373 }
374
375 fn min_price(&self) -> Option<Price> {
376 self.min_price
377 }
378
379 fn ts_event(&self) -> UnixNanos {
380 self.ts_event
381 }
382
383 fn ts_init(&self) -> UnixNanos {
384 self.ts_init
385 }
386
387 fn margin_init(&self) -> Decimal {
388 self.margin_init
389 }
390
391 fn margin_maint(&self) -> Decimal {
392 self.margin_maint
393 }
394
395 fn maker_fee(&self) -> Decimal {
396 self.maker_fee
397 }
398
399 fn taker_fee(&self) -> Decimal {
400 self.taker_fee
401 }
402
403 fn strike_price(&self) -> Option<Price> {
404 None
405 }
406
407 fn activation_ns(&self) -> Option<UnixNanos> {
408 Some(self.activation_ns)
409 }
410
411 fn expiration_ns(&self) -> Option<UnixNanos> {
412 Some(self.expiration_ns)
413 }
414
415 fn max_notional(&self) -> Option<Money> {
416 self.max_notional
417 }
418
419 fn min_notional(&self) -> Option<Money> {
420 self.min_notional
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use rstest::rstest;
427 use rust_decimal_macros::dec;
428
429 use crate::{
430 enums::{AssetClass, InstrumentClass},
431 identifiers::{InstrumentId, Symbol},
432 instruments::{CryptoFuture, Instrument, stubs::*},
433 types::{Currency, Money, Price, Quantity},
434 };
435
436 #[rstest]
437 fn test_trait_accessors(crypto_future_btcusdt: CryptoFuture) {
438 assert_eq!(
439 crypto_future_btcusdt.id(),
440 InstrumentId::from("ETHUSDT-123.BINANCE")
441 );
442 assert_eq!(
443 crypto_future_btcusdt.asset_class(),
444 AssetClass::Cryptocurrency
445 );
446 assert_eq!(
447 crypto_future_btcusdt.instrument_class(),
448 InstrumentClass::Future
449 );
450 assert_eq!(crypto_future_btcusdt.quote_currency(), Currency::USDT());
451 assert_eq!(
452 crypto_future_btcusdt.settlement_currency(),
453 Currency::USDT()
454 );
455 assert!(!crypto_future_btcusdt.is_inverse());
456 assert_eq!(crypto_future_btcusdt.price_precision(), 2);
457 assert_eq!(crypto_future_btcusdt.size_precision(), 6);
458 assert!(crypto_future_btcusdt.activation_ns().is_some());
459 assert!(crypto_future_btcusdt.expiration_ns().is_some());
460 }
461
462 #[rstest]
463 fn test_new_checked_price_precision_mismatch() {
464 let result = CryptoFuture::new_checked(
465 InstrumentId::from("TEST.BINANCE"),
466 Symbol::from("TEST"),
467 Currency::BTC(),
468 Currency::USDT(),
469 Currency::USDT(),
470 false,
471 0.into(),
472 0.into(),
473 4, 6,
475 Price::from("0.01"),
476 Quantity::from("0.000001"),
477 None,
478 None,
479 None,
480 None,
481 None,
482 None,
483 None,
484 None,
485 None,
486 None,
487 None,
488 None,
489 None,
490 None,
491 0.into(),
492 0.into(),
493 );
494 assert!(result.is_err());
495 }
496
497 #[rstest]
498 #[case::zero_multiplier(Some(Quantity::from("0")), None)]
499 #[case::zero_lot_size(None, Some(Quantity::from("0")))]
500 fn test_new_checked_rejects_non_positive_sizing(
501 #[case] multiplier: Option<Quantity>,
502 #[case] lot_size: Option<Quantity>,
503 ) {
504 let result = CryptoFuture::new_checked(
505 InstrumentId::from("TEST.BINANCE"),
506 Symbol::from("TEST"),
507 Currency::BTC(),
508 Currency::USDT(),
509 Currency::USDT(),
510 false,
511 0.into(),
512 0.into(),
513 2,
514 6,
515 Price::from("0.01"),
516 Quantity::from("0.000001"),
517 multiplier,
518 lot_size,
519 None,
520 None,
521 None,
522 None,
523 None,
524 None,
525 None,
526 None,
527 None,
528 None,
529 None,
530 None,
531 0.into(),
532 0.into(),
533 );
534 let error = result.unwrap_err();
535 assert!(error.to_string().contains("not positive"), "{error}");
536 }
537
538 #[rstest]
539 fn test_serialization_roundtrip(crypto_future_btcusdt: CryptoFuture) {
540 let json = serde_json::to_string(&crypto_future_btcusdt).unwrap();
541 let deserialized: CryptoFuture = serde_json::from_str(&json).unwrap();
542 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
543 }
544
545 #[rstest]
546 fn test_builder_matches_new_checked() {
547 let positional = CryptoFuture::new_checked(
548 InstrumentId::from("ETHUSDT-123.BINANCE"),
549 Symbol::from("BTCUSDT"),
550 Currency::BTC(),
551 Currency::USDT(),
552 Currency::USDC(),
553 false,
554 1.into(),
555 2.into(),
556 2,
557 6,
558 Price::from("0.01"),
559 Quantity::from("0.000001"),
560 Some(Quantity::from("10")),
561 Some(Quantity::from("1")),
562 Some(Quantity::from("9000.0")),
563 Some(Quantity::from("0.000001")),
564 Some(Money::new(5_000_000.0, Currency::USDT())),
565 Some(Money::new(10.0, Currency::USDT())),
566 Some(Price::from("1000000.00")),
567 Some(Price::from("0.01")),
568 Some(dec!(0.01)),
569 Some(dec!(0.02)),
570 Some(dec!(0.0002)),
571 Some(dec!(0.0004)),
572 None,
573 None,
574 10.into(),
575 20.into(),
576 )
577 .unwrap();
578
579 let built = CryptoFuture::builder()
580 .instrument_id(InstrumentId::from("ETHUSDT-123.BINANCE"))
581 .raw_symbol(Symbol::from("BTCUSDT"))
582 .underlying(Currency::BTC())
583 .quote_currency(Currency::USDT())
584 .settlement_currency(Currency::USDC())
585 .is_inverse(false)
586 .activation_ns(1.into())
587 .expiration_ns(2.into())
588 .price_precision(2)
589 .size_precision(6)
590 .price_increment(Price::from("0.01"))
591 .size_increment(Quantity::from("0.000001"))
592 .multiplier(Quantity::from("10"))
593 .lot_size(Quantity::from("1"))
594 .max_quantity(Quantity::from("9000.0"))
595 .min_quantity(Quantity::from("0.000001"))
596 .max_notional(Money::new(5_000_000.0, Currency::USDT()))
597 .min_notional(Money::new(10.0, Currency::USDT()))
598 .max_price(Price::from("1000000.00"))
599 .min_price(Price::from("0.01"))
600 .margin_init(dec!(0.01))
601 .margin_maint(dec!(0.02))
602 .maker_fee(dec!(0.0002))
603 .taker_fee(dec!(0.0004))
604 .ts_event(10.into())
605 .ts_init(20.into())
606 .build()
607 .unwrap();
608
609 assert_eq!(
610 serde_json::to_value(&positional).unwrap(),
611 serde_json::to_value(&built).unwrap(),
612 );
613 }
614}