1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{
21 CorrectnessResult, CorrectnessResultExt, FAILED, 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};
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.core.nautilus_pyo3.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 info: Option<Params>,
99 pub ts_event: UnixNanos,
101 pub ts_init: UnixNanos,
103}
104
105impl FuturesContract {
106 #[expect(clippy::too_many_arguments)]
115 pub fn new_checked(
116 instrument_id: InstrumentId,
117 raw_symbol: Symbol,
118 asset_class: AssetClass,
119 exchange: Option<Ustr>,
120 underlying: Ustr,
121 activation_ns: UnixNanos,
122 expiration_ns: UnixNanos,
123 currency: Currency,
124 price_precision: u8,
125 price_increment: Price,
126 multiplier: Quantity,
127 lot_size: Quantity,
128 max_quantity: Option<Quantity>,
129 min_quantity: Option<Quantity>,
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 info: Option<Params>,
137 ts_event: UnixNanos,
138 ts_init: UnixNanos,
139 ) -> CorrectnessResult<Self> {
140 check_valid_string_ascii_optional(exchange.map(|u| u.as_str()), stringify!(exchange))?;
141 check_valid_string_ascii(underlying.as_str(), stringify!(underlying))?;
142 check_equal_u8(
143 price_precision,
144 price_increment.precision,
145 stringify!(price_precision),
146 stringify!(price_increment.precision),
147 )?;
148 check_positive_price(price_increment, stringify!(price_increment))?;
149 check_positive_quantity(multiplier, stringify!(multiplier))?;
150 check_positive_quantity(lot_size, stringify!(lot_size))?;
151
152 Ok(Self {
153 id: instrument_id,
154 raw_symbol,
155 asset_class,
156 exchange,
157 underlying,
158 activation_ns,
159 expiration_ns,
160 currency,
161 price_precision,
162 price_increment,
163 size_precision: 0,
164 size_increment: Quantity::from(1),
165 multiplier,
166 lot_size,
167 max_quantity,
168 min_quantity: Some(min_quantity.unwrap_or(1.into())),
169 max_price,
170 min_price,
171 margin_init: margin_init.unwrap_or_default(),
172 margin_maint: margin_maint.unwrap_or_default(),
173 maker_fee: maker_fee.unwrap_or_default(),
174 taker_fee: taker_fee.unwrap_or_default(),
175 info,
176 ts_event,
177 ts_init,
178 })
179 }
180
181 #[expect(clippy::too_many_arguments)]
187 #[must_use]
188 pub fn new(
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 info: Option<Params>,
210 ts_event: UnixNanos,
211 ts_init: UnixNanos,
212 ) -> Self {
213 Self::new_checked(
214 instrument_id,
215 raw_symbol,
216 asset_class,
217 exchange,
218 underlying,
219 activation_ns,
220 expiration_ns,
221 currency,
222 price_precision,
223 price_increment,
224 multiplier,
225 lot_size,
226 max_quantity,
227 min_quantity,
228 max_price,
229 min_price,
230 margin_init,
231 margin_maint,
232 maker_fee,
233 taker_fee,
234 info,
235 ts_event,
236 ts_init,
237 )
238 .expect_display(FAILED)
239 }
240}
241
242impl PartialEq<Self> for FuturesContract {
243 fn eq(&self, other: &Self) -> bool {
244 self.id == other.id
245 }
246}
247
248impl Eq for FuturesContract {}
249
250impl Hash for FuturesContract {
251 fn hash<H: Hasher>(&self, state: &mut H) {
252 self.id.hash(state);
253 }
254}
255
256impl Instrument for FuturesContract {
257 fn into_any(self) -> InstrumentAny {
258 InstrumentAny::FuturesContract(self)
259 }
260
261 fn id(&self) -> InstrumentId {
262 self.id
263 }
264
265 fn raw_symbol(&self) -> Symbol {
266 self.raw_symbol
267 }
268
269 fn asset_class(&self) -> AssetClass {
270 self.asset_class
271 }
272
273 fn instrument_class(&self) -> InstrumentClass {
274 InstrumentClass::Future
275 }
276 fn underlying(&self) -> Option<Ustr> {
277 Some(self.underlying)
278 }
279
280 fn base_currency(&self) -> Option<Currency> {
281 None
282 }
283
284 fn quote_currency(&self) -> Currency {
285 self.currency
286 }
287
288 fn settlement_currency(&self) -> Currency {
289 self.currency
290 }
291
292 fn isin(&self) -> Option<Ustr> {
293 None
294 }
295
296 fn option_kind(&self) -> Option<OptionKind> {
297 None
298 }
299
300 fn exchange(&self) -> Option<Ustr> {
301 self.exchange
302 }
303
304 fn strike_price(&self) -> Option<Price> {
305 None
306 }
307
308 fn activation_ns(&self) -> Option<UnixNanos> {
309 Some(self.activation_ns)
310 }
311
312 fn expiration_ns(&self) -> Option<UnixNanos> {
313 Some(self.expiration_ns)
314 }
315
316 fn is_inverse(&self) -> bool {
317 false
318 }
319
320 fn price_precision(&self) -> u8 {
321 self.price_precision
322 }
323
324 fn size_precision(&self) -> u8 {
325 0
326 }
327
328 fn price_increment(&self) -> Price {
329 self.price_increment
330 }
331
332 fn size_increment(&self) -> Quantity {
333 Quantity::from(1)
334 }
335
336 fn multiplier(&self) -> Quantity {
337 self.multiplier
338 }
339
340 fn lot_size(&self) -> Option<Quantity> {
341 Some(self.lot_size)
342 }
343
344 fn max_quantity(&self) -> Option<Quantity> {
345 self.max_quantity
346 }
347
348 fn min_quantity(&self) -> Option<Quantity> {
349 self.min_quantity
350 }
351
352 fn max_notional(&self) -> Option<Money> {
353 None
354 }
355
356 fn min_notional(&self) -> Option<Money> {
357 None
358 }
359
360 fn max_price(&self) -> Option<Price> {
361 self.max_price
362 }
363
364 fn min_price(&self) -> Option<Price> {
365 self.min_price
366 }
367
368 fn ts_event(&self) -> UnixNanos {
369 self.ts_event
370 }
371
372 fn ts_init(&self) -> UnixNanos {
373 self.ts_init
374 }
375
376 fn margin_init(&self) -> Decimal {
377 self.margin_init
378 }
379
380 fn margin_maint(&self) -> Decimal {
381 self.margin_maint
382 }
383
384 fn maker_fee(&self) -> Decimal {
385 self.maker_fee
386 }
387
388 fn taker_fee(&self) -> Decimal {
389 self.taker_fee
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use rstest::rstest;
396 use ustr::Ustr;
397
398 use crate::{
399 enums::{AssetClass, InstrumentClass},
400 identifiers::{InstrumentId, Symbol},
401 instruments::{FuturesContract, Instrument, stubs::*},
402 types::{Currency, Price, Quantity},
403 };
404
405 #[rstest]
406 fn test_trait_accessors() {
407 let inst = futures_contract_es(None, None);
408 assert_eq!(inst.id(), InstrumentId::from("ESZ21.GLBX"));
409 assert_eq!(inst.raw_symbol(), Symbol::from("ESZ21"));
410 assert_eq!(inst.asset_class(), AssetClass::Index);
411 assert_eq!(inst.instrument_class(), InstrumentClass::Future);
412 assert_eq!(inst.quote_currency(), Currency::USD());
413 assert!(!inst.is_inverse());
414 assert_eq!(inst.price_precision(), 2);
415 assert_eq!(inst.size_precision(), 0);
416 assert_eq!(inst.price_increment(), Price::from("0.01"));
417 assert_eq!(inst.size_increment(), Quantity::from("1"));
418 assert_eq!(inst.multiplier(), Quantity::from("1"));
419 assert_eq!(inst.lot_size(), Some(Quantity::from("1")));
420 assert_eq!(inst.underlying(), Some(Ustr::from("ES")));
421 assert_eq!(inst.exchange(), Some(Ustr::from("XCME")));
422 assert!(inst.activation_ns().is_some());
423 assert!(inst.expiration_ns().is_some());
424 assert_eq!(inst.min_quantity(), Some(Quantity::from("1")));
425 }
426
427 #[rstest]
428 fn test_new_checked_price_precision_mismatch() {
429 let result = FuturesContract::new_checked(
430 InstrumentId::from("ESZ21.GLBX"),
431 Symbol::from("ESZ21"),
432 AssetClass::Index,
433 Some(Ustr::from("XCME")),
434 Ustr::from("ES"),
435 0.into(),
436 0.into(),
437 Currency::USD(),
438 4, Price::from("0.01"),
440 Quantity::from(1),
441 Quantity::from(1),
442 None,
443 None,
444 None,
445 None,
446 None,
447 None,
448 None,
449 None,
450 None,
451 0.into(),
452 0.into(),
453 );
454 assert!(result.is_err());
455 }
456
457 #[rstest]
458 fn test_new_checked_zero_multiplier() {
459 let result = FuturesContract::new_checked(
460 InstrumentId::from("ESZ21.GLBX"),
461 Symbol::from("ESZ21"),
462 AssetClass::Index,
463 Some(Ustr::from("XCME")),
464 Ustr::from("ES"),
465 0.into(),
466 0.into(),
467 Currency::USD(),
468 2,
469 Price::from("0.01"),
470 Quantity::from("0"), Quantity::from(1),
472 None,
473 None,
474 None,
475 None,
476 None,
477 None,
478 None,
479 None,
480 None,
481 0.into(),
482 0.into(),
483 );
484 assert!(result.is_err());
485 }
486
487 #[rstest]
488 fn test_new_checked_zero_lot_size() {
489 let result = FuturesContract::new_checked(
490 InstrumentId::from("ESZ21.GLBX"),
491 Symbol::from("ESZ21"),
492 AssetClass::Index,
493 Some(Ustr::from("XCME")),
494 Ustr::from("ES"),
495 0.into(),
496 0.into(),
497 Currency::USD(),
498 2,
499 Price::from("0.01"),
500 Quantity::from(1),
501 Quantity::from("0"), None,
503 None,
504 None,
505 None,
506 None,
507 None,
508 None,
509 None,
510 None,
511 0.into(),
512 0.into(),
513 );
514 assert!(result.is_err());
515 }
516
517 #[rstest]
518 fn test_serialization_roundtrip() {
519 let inst = futures_contract_es(None, None);
520 let json = serde_json::to_string(&inst).unwrap();
521 let deserialized: FuturesContract = serde_json::from_str(&json).unwrap();
522 assert_eq!(inst, deserialized);
523 }
524}