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