1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{
21 CorrectnessResult, check_equal_u8, check_valid_string_ascii,
22 check_valid_string_ascii_optional,
23 },
24};
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use super::{Instrument, any::InstrumentAny, tick_scheme::check_tick_scheme};
30use crate::{
31 enums::{AssetClass, InstrumentClass, OptionKind},
32 identifiers::{InstrumentId, Symbol},
33 types::{
34 currency::Currency,
35 money::Money,
36 price::{Price, check_positive_price},
37 quantity::{Quantity, check_positive_quantity},
38 },
39};
40
41#[repr(C)]
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
51)]
52pub struct FuturesContract {
53 pub id: InstrumentId,
55 pub raw_symbol: Symbol,
57 pub asset_class: AssetClass,
59 pub exchange: Option<Ustr>,
61 pub underlying: Ustr,
63 pub activation_ns: UnixNanos,
65 pub expiration_ns: UnixNanos,
67 pub currency: Currency,
69 pub price_precision: u8,
71 pub price_increment: Price,
73 pub size_increment: Quantity,
75 pub size_precision: u8,
77 pub multiplier: Quantity,
79 pub lot_size: Quantity,
81 pub margin_init: Decimal,
83 pub margin_maint: Decimal,
85 pub maker_fee: Decimal,
87 pub taker_fee: Decimal,
89 pub max_quantity: Option<Quantity>,
91 pub min_quantity: Option<Quantity>,
93 pub max_price: Option<Price>,
95 pub min_price: Option<Price>,
97 pub tick_scheme: Option<Ustr>,
99 pub info: Option<Params>,
101 pub ts_event: UnixNanos,
103 pub ts_init: UnixNanos,
105}
106
107#[bon::bon]
108impl FuturesContract {
109 #[expect(clippy::too_many_arguments)]
110 fn new_checked(
111 instrument_id: InstrumentId,
112 raw_symbol: Symbol,
113 asset_class: AssetClass,
114 exchange: Option<Ustr>,
115 underlying: Ustr,
116 activation_ns: UnixNanos,
117 expiration_ns: UnixNanos,
118 currency: Currency,
119 price_precision: u8,
120 price_increment: Price,
121 multiplier: Quantity,
122 lot_size: Quantity,
123 max_quantity: Option<Quantity>,
124 min_quantity: Option<Quantity>,
125 max_price: Option<Price>,
126 min_price: Option<Price>,
127 margin_init: Option<Decimal>,
128 margin_maint: Option<Decimal>,
129 maker_fee: Option<Decimal>,
130 taker_fee: Option<Decimal>,
131 tick_scheme: Option<Ustr>,
132 info: Option<Params>,
133 ts_event: UnixNanos,
134 ts_init: UnixNanos,
135 ) -> CorrectnessResult<Self> {
136 check_valid_string_ascii_optional(exchange, stringify!(exchange))?;
137 check_valid_string_ascii(underlying, stringify!(underlying))?;
138 check_equal_u8(
139 price_precision,
140 price_increment.precision,
141 stringify!(price_precision),
142 stringify!(price_increment.precision),
143 )?;
144 check_positive_price(price_increment, stringify!(price_increment))?;
145 check_tick_scheme(tick_scheme)?;
146 check_positive_quantity(multiplier, stringify!(multiplier))?;
147 check_positive_quantity(lot_size, stringify!(lot_size))?;
148
149 Ok(Self {
150 id: instrument_id,
151 raw_symbol,
152 asset_class,
153 exchange,
154 underlying,
155 activation_ns,
156 expiration_ns,
157 currency,
158 price_precision,
159 price_increment,
160 size_precision: 0,
161 size_increment: Quantity::from(1),
162 multiplier,
163 lot_size,
164 max_quantity,
165 min_quantity: Some(min_quantity.unwrap_or(1.into())),
166 max_price,
167 min_price,
168 margin_init: margin_init.unwrap_or_default(),
169 margin_maint: margin_maint.unwrap_or_default(),
170 maker_fee: maker_fee.unwrap_or_default(),
171 taker_fee: taker_fee.unwrap_or_default(),
172 tick_scheme,
173 info,
174 ts_event,
175 ts_init,
176 })
177 }
178
179 #[builder(start_fn = builder, finish_fn = build)]
188 pub fn build_checked(
189 instrument_id: InstrumentId,
190 raw_symbol: Symbol,
191 asset_class: AssetClass,
192 exchange: Option<Ustr>,
193 underlying: Ustr,
194 activation_ns: UnixNanos,
195 expiration_ns: UnixNanos,
196 currency: Currency,
197 price_precision: u8,
198 price_increment: Price,
199 multiplier: Quantity,
200 lot_size: Quantity,
201 max_quantity: Option<Quantity>,
202 min_quantity: Option<Quantity>,
203 max_price: Option<Price>,
204 min_price: Option<Price>,
205 margin_init: Option<Decimal>,
206 margin_maint: Option<Decimal>,
207 maker_fee: Option<Decimal>,
208 taker_fee: Option<Decimal>,
209 tick_scheme: Option<Ustr>,
210 info: Option<Params>,
211 ts_event: UnixNanos,
212 ts_init: UnixNanos,
213 ) -> CorrectnessResult<Self> {
214 Self::new_checked(
215 instrument_id,
216 raw_symbol,
217 asset_class,
218 exchange,
219 underlying,
220 activation_ns,
221 expiration_ns,
222 currency,
223 price_precision,
224 price_increment,
225 multiplier,
226 lot_size,
227 max_quantity,
228 min_quantity,
229 max_price,
230 min_price,
231 margin_init,
232 margin_maint,
233 maker_fee,
234 taker_fee,
235 tick_scheme,
236 info,
237 ts_event,
238 ts_init,
239 )
240 }
241}
242
243impl PartialEq<Self> for FuturesContract {
244 fn eq(&self, other: &Self) -> bool {
245 self.id == other.id
246 }
247}
248
249impl Eq for FuturesContract {}
250
251impl Hash for FuturesContract {
252 fn hash<H: Hasher>(&self, state: &mut H) {
253 self.id.hash(state);
254 }
255}
256
257impl Instrument for FuturesContract {
258 fn into_any(self) -> InstrumentAny {
259 InstrumentAny::FuturesContract(self)
260 }
261
262 fn id(&self) -> InstrumentId {
263 self.id
264 }
265
266 fn raw_symbol(&self) -> Symbol {
267 self.raw_symbol
268 }
269
270 fn asset_class(&self) -> AssetClass {
271 self.asset_class
272 }
273
274 fn instrument_class(&self) -> InstrumentClass {
275 InstrumentClass::Future
276 }
277 fn underlying(&self) -> Option<Ustr> {
278 Some(self.underlying)
279 }
280
281 fn base_currency(&self) -> Option<Currency> {
282 None
283 }
284
285 fn quote_currency(&self) -> Currency {
286 self.currency
287 }
288
289 fn settlement_currency(&self) -> Currency {
290 self.currency
291 }
292
293 fn isin(&self) -> Option<Ustr> {
294 None
295 }
296
297 fn option_kind(&self) -> Option<OptionKind> {
298 None
299 }
300
301 fn exchange(&self) -> Option<Ustr> {
302 self.exchange
303 }
304
305 fn strike_price(&self) -> Option<Price> {
306 None
307 }
308
309 fn activation_ns(&self) -> Option<UnixNanos> {
310 Some(self.activation_ns)
311 }
312
313 fn expiration_ns(&self) -> Option<UnixNanos> {
314 Some(self.expiration_ns)
315 }
316
317 fn is_inverse(&self) -> bool {
318 false
319 }
320
321 fn price_precision(&self) -> u8 {
322 self.price_precision
323 }
324
325 fn size_precision(&self) -> u8 {
326 0
327 }
328
329 fn price_increment(&self) -> Price {
330 self.price_increment
331 }
332
333 fn size_increment(&self) -> Quantity {
334 Quantity::from(1)
335 }
336
337 fn multiplier(&self) -> Quantity {
338 self.multiplier
339 }
340
341 fn lot_size(&self) -> Option<Quantity> {
342 Some(self.lot_size)
343 }
344
345 fn max_quantity(&self) -> Option<Quantity> {
346 self.max_quantity
347 }
348
349 fn min_quantity(&self) -> Option<Quantity> {
350 self.min_quantity
351 }
352
353 fn max_notional(&self) -> Option<Money> {
354 None
355 }
356
357 fn min_notional(&self) -> Option<Money> {
358 None
359 }
360
361 fn max_price(&self) -> Option<Price> {
362 self.max_price
363 }
364
365 fn min_price(&self) -> Option<Price> {
366 self.min_price
367 }
368
369 fn tick_scheme(&self) -> Option<Ustr> {
370 self.tick_scheme
371 }
372
373 fn info(&self) -> Option<&Params> {
374 self.info.as_ref()
375 }
376
377 fn ts_event(&self) -> UnixNanos {
378 self.ts_event
379 }
380
381 fn ts_init(&self) -> UnixNanos {
382 self.ts_init
383 }
384
385 fn margin_init(&self) -> Decimal {
386 self.margin_init
387 }
388
389 fn margin_maint(&self) -> Decimal {
390 self.margin_maint
391 }
392
393 fn maker_fee(&self) -> Decimal {
394 self.maker_fee
395 }
396
397 fn taker_fee(&self) -> Decimal {
398 self.taker_fee
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use rstest::rstest;
405 use rust_decimal_macros::dec;
406 use ustr::Ustr;
407
408 use crate::{
409 enums::{AssetClass, InstrumentClass},
410 identifiers::{InstrumentId, Symbol},
411 instruments::{FuturesContract, Instrument, stubs::*},
412 types::{Currency, Price, Quantity},
413 };
414
415 #[rstest]
416 fn test_trait_accessors() {
417 let inst = futures_contract_es(None, None);
418 assert_eq!(inst.id(), InstrumentId::from("ESZ21.GLBX"));
419 assert_eq!(inst.raw_symbol(), Symbol::from("ESZ21"));
420 assert_eq!(inst.asset_class(), AssetClass::Index);
421 assert_eq!(inst.instrument_class(), InstrumentClass::Future);
422 assert_eq!(inst.quote_currency(), Currency::USD());
423 assert!(!inst.is_inverse());
424 assert_eq!(inst.price_precision(), 2);
425 assert_eq!(inst.size_precision(), 0);
426 assert_eq!(inst.price_increment(), Price::from("0.01"));
427 assert_eq!(inst.size_increment(), Quantity::from("1"));
428 assert_eq!(inst.multiplier(), Quantity::from("1"));
429 assert_eq!(inst.lot_size(), Some(Quantity::from("1")));
430 assert_eq!(inst.underlying(), Some(Ustr::from("ES")));
431 assert_eq!(inst.exchange(), Some(Ustr::from("XCME")));
432 assert!(inst.activation_ns().is_some());
433 assert!(inst.expiration_ns().is_some());
434 assert_eq!(inst.min_quantity(), Some(Quantity::from("1")));
435 }
436
437 #[rstest]
438 fn test_new_checked_price_precision_mismatch() {
439 let result = FuturesContract::new_checked(
440 InstrumentId::from("ESZ21.GLBX"),
441 Symbol::from("ESZ21"),
442 AssetClass::Index,
443 Some(Ustr::from("XCME")),
444 Ustr::from("ES"),
445 0.into(),
446 0.into(),
447 Currency::USD(),
448 4, Price::from("0.01"),
450 Quantity::from(1),
451 Quantity::from(1),
452 None,
453 None,
454 None,
455 None,
456 None,
457 None,
458 None,
459 None,
460 None,
461 None,
462 0.into(),
463 0.into(),
464 );
465 assert!(result.is_err());
466 }
467
468 #[rstest]
469 fn test_new_checked_zero_multiplier() {
470 let result = FuturesContract::new_checked(
471 InstrumentId::from("ESZ21.GLBX"),
472 Symbol::from("ESZ21"),
473 AssetClass::Index,
474 Some(Ustr::from("XCME")),
475 Ustr::from("ES"),
476 0.into(),
477 0.into(),
478 Currency::USD(),
479 2,
480 Price::from("0.01"),
481 Quantity::from("0"), Quantity::from(1),
483 None,
484 None,
485 None,
486 None,
487 None,
488 None,
489 None,
490 None,
491 None,
492 None,
493 0.into(),
494 0.into(),
495 );
496 assert!(result.is_err());
497 }
498
499 #[rstest]
500 fn test_new_checked_zero_lot_size() {
501 let result = FuturesContract::new_checked(
502 InstrumentId::from("ESZ21.GLBX"),
503 Symbol::from("ESZ21"),
504 AssetClass::Index,
505 Some(Ustr::from("XCME")),
506 Ustr::from("ES"),
507 0.into(),
508 0.into(),
509 Currency::USD(),
510 2,
511 Price::from("0.01"),
512 Quantity::from(1),
513 Quantity::from("0"), None,
515 None,
516 None,
517 None,
518 None,
519 None,
520 None,
521 None,
522 None,
523 None,
524 0.into(),
525 0.into(),
526 );
527 assert!(result.is_err());
528 }
529
530 #[rstest]
531 fn test_serialization_roundtrip() {
532 let inst = futures_contract_es(None, None);
533 let json = serde_json::to_string(&inst).unwrap();
534 let deserialized: FuturesContract = serde_json::from_str(&json).unwrap();
535 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
536 }
537
538 #[rstest]
539 fn test_builder_matches_new_checked() {
540 let positional = FuturesContract::new_checked(
541 InstrumentId::from("ESZ21.GLBX"),
542 Symbol::from("ESZ21"),
543 AssetClass::Index,
544 Some(Ustr::from("XCME")),
545 Ustr::from("ES"),
546 1_000.into(),
547 2_000.into(),
548 Currency::USD(),
549 2,
550 Price::from("0.01"),
551 Quantity::from(50),
552 Quantity::from(10),
553 Some(Quantity::from("10000")),
554 Some(Quantity::from("5")),
555 Some(Price::from("9999.99")),
556 Some(Price::from("0.01")),
557 Some(dec!(0.01)),
558 Some(dec!(0.02)),
559 Some(dec!(0.0002)),
560 Some(dec!(0.0004)),
561 None,
562 None,
563 1.into(),
564 2.into(),
565 )
566 .unwrap();
567
568 let built = FuturesContract::builder()
569 .instrument_id(InstrumentId::from("ESZ21.GLBX"))
570 .raw_symbol(Symbol::from("ESZ21"))
571 .asset_class(AssetClass::Index)
572 .exchange(Ustr::from("XCME"))
573 .underlying(Ustr::from("ES"))
574 .activation_ns(1_000.into())
575 .expiration_ns(2_000.into())
576 .currency(Currency::USD())
577 .price_precision(2)
578 .price_increment(Price::from("0.01"))
579 .multiplier(Quantity::from(50))
580 .lot_size(Quantity::from(10))
581 .max_quantity(Quantity::from("10000"))
582 .min_quantity(Quantity::from("5"))
583 .max_price(Price::from("9999.99"))
584 .min_price(Price::from("0.01"))
585 .margin_init(dec!(0.01))
586 .margin_maint(dec!(0.02))
587 .maker_fee(dec!(0.0002))
588 .taker_fee(dec!(0.0004))
589 .ts_event(1.into())
590 .ts_init(2.into())
591 .build()
592 .unwrap();
593
594 assert_eq!(
595 serde_json::to_value(&positional).unwrap(),
596 serde_json::to_value(&built).unwrap(),
597 );
598 }
599}