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