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