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 into_any(self) -> InstrumentAny {
285 InstrumentAny::CryptoFuture(self)
286 }
287
288 fn id(&self) -> InstrumentId {
289 self.id
290 }
291
292 fn raw_symbol(&self) -> Symbol {
293 self.raw_symbol
294 }
295
296 fn asset_class(&self) -> AssetClass {
297 AssetClass::Cryptocurrency
298 }
299
300 fn instrument_class(&self) -> InstrumentClass {
301 InstrumentClass::Future
302 }
303
304 fn underlying(&self) -> Option<Ustr> {
305 Some(self.underlying.code)
306 }
307
308 fn base_currency(&self) -> Option<Currency> {
309 Some(self.underlying)
310 }
311
312 fn quote_currency(&self) -> Currency {
313 self.quote_currency
314 }
315
316 fn settlement_currency(&self) -> Currency {
317 self.settlement_currency
318 }
319
320 fn isin(&self) -> Option<Ustr> {
321 None
322 }
323
324 fn exchange(&self) -> Option<Ustr> {
325 None
326 }
327
328 fn option_kind(&self) -> Option<OptionKind> {
329 None
330 }
331
332 fn is_inverse(&self) -> bool {
333 self.is_inverse
334 }
335
336 fn price_precision(&self) -> u8 {
337 self.price_precision
338 }
339
340 fn size_precision(&self) -> u8 {
341 self.size_precision
342 }
343
344 fn price_increment(&self) -> Price {
345 self.price_increment
346 }
347
348 fn size_increment(&self) -> Quantity {
349 self.size_increment
350 }
351
352 fn multiplier(&self) -> Quantity {
353 self.multiplier
354 }
355
356 fn lot_size(&self) -> Option<Quantity> {
357 Some(self.lot_size)
358 }
359
360 fn max_quantity(&self) -> Option<Quantity> {
361 self.max_quantity
362 }
363
364 fn min_quantity(&self) -> Option<Quantity> {
365 self.min_quantity
366 }
367
368 fn max_price(&self) -> Option<Price> {
369 self.max_price
370 }
371
372 fn min_price(&self) -> Option<Price> {
373 self.min_price
374 }
375
376 fn tick_scheme(&self) -> Option<Ustr> {
377 self.tick_scheme
378 }
379
380 fn info(&self) -> Option<&Params> {
381 self.info.as_ref()
382 }
383
384 fn ts_event(&self) -> UnixNanos {
385 self.ts_event
386 }
387
388 fn ts_init(&self) -> UnixNanos {
389 self.ts_init
390 }
391
392 fn margin_init(&self) -> Decimal {
393 self.margin_init
394 }
395
396 fn margin_maint(&self) -> Decimal {
397 self.margin_maint
398 }
399
400 fn maker_fee(&self) -> Decimal {
401 self.maker_fee
402 }
403
404 fn taker_fee(&self) -> Decimal {
405 self.taker_fee
406 }
407
408 fn strike_price(&self) -> Option<Price> {
409 None
410 }
411
412 fn activation_ns(&self) -> Option<UnixNanos> {
413 Some(self.activation_ns)
414 }
415
416 fn expiration_ns(&self) -> Option<UnixNanos> {
417 Some(self.expiration_ns)
418 }
419
420 fn max_notional(&self) -> Option<Money> {
421 self.max_notional
422 }
423
424 fn min_notional(&self) -> Option<Money> {
425 self.min_notional
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use rstest::rstest;
432 use rust_decimal_macros::dec;
433
434 use crate::{
435 enums::{AssetClass, InstrumentClass},
436 identifiers::{InstrumentId, Symbol},
437 instruments::{CryptoFuture, Instrument, stubs::*},
438 types::{Currency, Money, Price, Quantity},
439 };
440
441 #[rstest]
442 fn test_trait_accessors(crypto_future_btcusdt: CryptoFuture) {
443 assert_eq!(
444 crypto_future_btcusdt.id(),
445 InstrumentId::from("ETHUSDT-123.BINANCE")
446 );
447 assert_eq!(
448 crypto_future_btcusdt.asset_class(),
449 AssetClass::Cryptocurrency
450 );
451 assert_eq!(
452 crypto_future_btcusdt.instrument_class(),
453 InstrumentClass::Future
454 );
455 assert_eq!(crypto_future_btcusdt.quote_currency(), Currency::USDT());
456 assert_eq!(
457 crypto_future_btcusdt.settlement_currency(),
458 Currency::USDT()
459 );
460 assert!(!crypto_future_btcusdt.is_inverse());
461 assert_eq!(crypto_future_btcusdt.price_precision(), 2);
462 assert_eq!(crypto_future_btcusdt.size_precision(), 6);
463 assert!(crypto_future_btcusdt.activation_ns().is_some());
464 assert!(crypto_future_btcusdt.expiration_ns().is_some());
465 }
466
467 #[rstest]
468 fn test_new_checked_price_precision_mismatch() {
469 let result = CryptoFuture::new_checked(
470 InstrumentId::from("TEST.BINANCE"),
471 Symbol::from("TEST"),
472 Currency::BTC(),
473 Currency::USDT(),
474 Currency::USDT(),
475 false,
476 0.into(),
477 0.into(),
478 4, 6,
480 Price::from("0.01"),
481 Quantity::from("0.000001"),
482 None,
483 None,
484 None,
485 None,
486 None,
487 None,
488 None,
489 None,
490 None,
491 None,
492 None,
493 None,
494 None,
495 None,
496 0.into(),
497 0.into(),
498 );
499 assert!(result.is_err());
500 }
501
502 #[rstest]
503 #[case::zero_multiplier(Some(Quantity::from("0")), None)]
504 #[case::zero_lot_size(None, Some(Quantity::from("0")))]
505 fn test_new_checked_rejects_non_positive_sizing(
506 #[case] multiplier: Option<Quantity>,
507 #[case] lot_size: Option<Quantity>,
508 ) {
509 let result = CryptoFuture::new_checked(
510 InstrumentId::from("TEST.BINANCE"),
511 Symbol::from("TEST"),
512 Currency::BTC(),
513 Currency::USDT(),
514 Currency::USDT(),
515 false,
516 0.into(),
517 0.into(),
518 2,
519 6,
520 Price::from("0.01"),
521 Quantity::from("0.000001"),
522 multiplier,
523 lot_size,
524 None,
525 None,
526 None,
527 None,
528 None,
529 None,
530 None,
531 None,
532 None,
533 None,
534 None,
535 None,
536 0.into(),
537 0.into(),
538 );
539 let error = result.unwrap_err();
540 assert!(error.to_string().contains("not positive"), "{error}");
541 }
542
543 #[rstest]
544 fn test_serialization_roundtrip(crypto_future_btcusdt: CryptoFuture) {
545 let json = serde_json::to_string(&crypto_future_btcusdt).unwrap();
546 let deserialized: CryptoFuture = serde_json::from_str(&json).unwrap();
547 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
548 }
549
550 #[rstest]
551 fn test_builder_matches_new_checked() {
552 let positional = CryptoFuture::new_checked(
553 InstrumentId::from("ETHUSDT-123.BINANCE"),
554 Symbol::from("BTCUSDT"),
555 Currency::BTC(),
556 Currency::USDT(),
557 Currency::USDC(),
558 false,
559 1.into(),
560 2.into(),
561 2,
562 6,
563 Price::from("0.01"),
564 Quantity::from("0.000001"),
565 Some(Quantity::from("10")),
566 Some(Quantity::from("1")),
567 Some(Quantity::from("9000.0")),
568 Some(Quantity::from("0.000001")),
569 Some(Money::new(5_000_000.0, Currency::USDT())),
570 Some(Money::new(10.0, Currency::USDT())),
571 Some(Price::from("1000000.00")),
572 Some(Price::from("0.01")),
573 Some(dec!(0.01)),
574 Some(dec!(0.02)),
575 Some(dec!(0.0002)),
576 Some(dec!(0.0004)),
577 None,
578 None,
579 10.into(),
580 20.into(),
581 )
582 .unwrap();
583
584 let built = CryptoFuture::builder()
585 .instrument_id(InstrumentId::from("ETHUSDT-123.BINANCE"))
586 .raw_symbol(Symbol::from("BTCUSDT"))
587 .underlying(Currency::BTC())
588 .quote_currency(Currency::USDT())
589 .settlement_currency(Currency::USDC())
590 .is_inverse(false)
591 .activation_ns(1.into())
592 .expiration_ns(2.into())
593 .price_precision(2)
594 .size_precision(6)
595 .price_increment(Price::from("0.01"))
596 .size_increment(Quantity::from("0.000001"))
597 .multiplier(Quantity::from("10"))
598 .lot_size(Quantity::from("1"))
599 .max_quantity(Quantity::from("9000.0"))
600 .min_quantity(Quantity::from("0.000001"))
601 .max_notional(Money::new(5_000_000.0, Currency::USDT()))
602 .min_notional(Money::new(10.0, Currency::USDT()))
603 .max_price(Price::from("1000000.00"))
604 .min_price(Price::from("0.01"))
605 .margin_init(dec!(0.01))
606 .margin_maint(dec!(0.02))
607 .maker_fee(dec!(0.0002))
608 .taker_fee(dec!(0.0004))
609 .ts_event(10.into())
610 .ts_init(20.into())
611 .build()
612 .unwrap();
613
614 assert_eq!(
615 serde_json::to_value(&positional).unwrap(),
616 serde_json::to_value(&built).unwrap(),
617 );
618 }
619}