nautilus_model/instruments/
index_instrument.rs1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{CorrectnessResult, CorrectnessResultExt, FAILED, check_equal_u8},
21};
22use serde::{Deserialize, Serialize};
23use ustr::Ustr;
24
25use super::{Instrument, any::InstrumentAny, tick_scheme::check_tick_scheme};
26use crate::{
27 enums::{AssetClass, InstrumentClass, OptionKind},
28 identifiers::{InstrumentId, Symbol},
29 types::{
30 currency::Currency,
31 money::Money,
32 price::{Price, check_positive_price},
33 quantity::{Quantity, check_positive_quantity},
34 },
35};
36
37#[repr(C)]
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
45)]
46#[cfg_attr(
47 feature = "python",
48 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
49)]
50pub struct IndexInstrument {
51 pub id: InstrumentId,
53 pub raw_symbol: Symbol,
55 pub currency: Currency,
57 pub price_precision: u8,
59 pub size_precision: u8,
61 pub price_increment: Price,
63 pub size_increment: Quantity,
65 pub tick_scheme: Option<Ustr>,
67 pub info: Option<Params>,
69 pub ts_event: UnixNanos,
71 pub ts_init: UnixNanos,
73}
74
75#[bon::bon]
76impl IndexInstrument {
77 #[expect(clippy::too_many_arguments)]
87 pub fn new_checked(
88 instrument_id: InstrumentId,
89 raw_symbol: Symbol,
90 currency: Currency,
91 price_precision: u8,
92 size_precision: u8,
93 price_increment: Price,
94 size_increment: Quantity,
95 tick_scheme: Option<Ustr>,
96 info: Option<Params>,
97 ts_event: UnixNanos,
98 ts_init: UnixNanos,
99 ) -> CorrectnessResult<Self> {
100 check_equal_u8(
101 price_precision,
102 price_increment.precision,
103 stringify!(price_precision),
104 stringify!(price_increment.precision),
105 )?;
106 check_equal_u8(
107 size_precision,
108 size_increment.precision,
109 stringify!(size_precision),
110 stringify!(size_increment.precision),
111 )?;
112 check_positive_price(price_increment, stringify!(price_increment))?;
113 check_positive_quantity(size_increment, stringify!(size_increment))?;
114 check_tick_scheme(tick_scheme)?;
115
116 Ok(Self {
117 id: instrument_id,
118 raw_symbol,
119 currency,
120 price_precision,
121 size_precision,
122 price_increment,
123 size_increment,
124 tick_scheme,
125 info,
126 ts_event,
127 ts_init,
128 })
129 }
130
131 #[expect(clippy::too_many_arguments)]
137 #[must_use]
138 pub fn new(
139 instrument_id: InstrumentId,
140 raw_symbol: Symbol,
141 currency: Currency,
142 price_precision: u8,
143 size_precision: u8,
144 price_increment: Price,
145 size_increment: Quantity,
146 tick_scheme: Option<Ustr>,
147 info: Option<Params>,
148 ts_event: UnixNanos,
149 ts_init: UnixNanos,
150 ) -> Self {
151 Self::new_checked(
152 instrument_id,
153 raw_symbol,
154 currency,
155 price_precision,
156 size_precision,
157 price_increment,
158 size_increment,
159 tick_scheme,
160 info,
161 ts_event,
162 ts_init,
163 )
164 .expect_display(FAILED)
165 }
166
167 #[builder(start_fn = builder, finish_fn = build)]
177 pub fn build_checked(
178 instrument_id: InstrumentId,
179 raw_symbol: Symbol,
180 currency: Currency,
181 price_precision: u8,
182 size_precision: u8,
183 price_increment: Price,
184 size_increment: Quantity,
185 tick_scheme: Option<Ustr>,
186 info: Option<Params>,
187 ts_event: UnixNanos,
188 ts_init: UnixNanos,
189 ) -> CorrectnessResult<Self> {
190 Self::new_checked(
191 instrument_id,
192 raw_symbol,
193 currency,
194 price_precision,
195 size_precision,
196 price_increment,
197 size_increment,
198 tick_scheme,
199 info,
200 ts_event,
201 ts_init,
202 )
203 }
204}
205
206impl PartialEq<Self> for IndexInstrument {
207 fn eq(&self, other: &Self) -> bool {
208 self.id == other.id
209 }
210}
211
212impl Eq for IndexInstrument {}
213
214impl Hash for IndexInstrument {
215 fn hash<H: Hasher>(&self, state: &mut H) {
216 self.id.hash(state);
217 }
218}
219
220impl Instrument for IndexInstrument {
221 fn tick_scheme(&self) -> Option<Ustr> {
222 self.tick_scheme
223 }
224 fn into_any(self) -> InstrumentAny {
225 InstrumentAny::IndexInstrument(self)
226 }
227
228 fn id(&self) -> InstrumentId {
229 self.id
230 }
231
232 fn raw_symbol(&self) -> Symbol {
233 self.raw_symbol
234 }
235
236 fn asset_class(&self) -> AssetClass {
237 AssetClass::Index
238 }
239
240 fn instrument_class(&self) -> InstrumentClass {
241 InstrumentClass::Spot
242 }
243
244 fn underlying(&self) -> Option<Ustr> {
245 None
246 }
247
248 fn base_currency(&self) -> Option<Currency> {
249 None
250 }
251
252 fn quote_currency(&self) -> Currency {
253 self.currency
254 }
255
256 fn settlement_currency(&self) -> Currency {
257 self.currency
258 }
259
260 fn isin(&self) -> Option<Ustr> {
261 None
262 }
263
264 fn option_kind(&self) -> Option<OptionKind> {
265 None
266 }
267
268 fn exchange(&self) -> Option<Ustr> {
269 None
270 }
271
272 fn strike_price(&self) -> Option<Price> {
273 None
274 }
275
276 fn activation_ns(&self) -> Option<UnixNanos> {
277 None
278 }
279
280 fn expiration_ns(&self) -> Option<UnixNanos> {
281 None
282 }
283
284 fn is_inverse(&self) -> bool {
285 false
286 }
287
288 fn price_precision(&self) -> u8 {
289 self.price_precision
290 }
291
292 fn size_precision(&self) -> u8 {
293 self.size_precision
294 }
295
296 fn price_increment(&self) -> Price {
297 self.price_increment
298 }
299
300 fn size_increment(&self) -> Quantity {
301 self.size_increment
302 }
303
304 fn multiplier(&self) -> Quantity {
305 Quantity::from(1)
306 }
307
308 fn lot_size(&self) -> Option<Quantity> {
309 None
310 }
311
312 fn max_quantity(&self) -> Option<Quantity> {
313 None
314 }
315
316 fn min_quantity(&self) -> Option<Quantity> {
317 None
318 }
319
320 fn max_notional(&self) -> Option<Money> {
321 None
322 }
323
324 fn min_notional(&self) -> Option<Money> {
325 None
326 }
327
328 fn max_price(&self) -> Option<Price> {
329 None
330 }
331
332 fn min_price(&self) -> Option<Price> {
333 None
334 }
335
336 fn ts_event(&self) -> UnixNanos {
337 self.ts_event
338 }
339
340 fn ts_init(&self) -> UnixNanos {
341 self.ts_init
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use rstest::rstest;
348
349 use crate::{
350 enums::{AssetClass, InstrumentClass},
351 identifiers::{InstrumentId, Symbol},
352 instruments::{IndexInstrument, Instrument, stubs::*},
353 types::{Currency, Price, Quantity},
354 };
355
356 #[rstest]
357 fn test_trait_accessors(index_instrument_spx: IndexInstrument) {
358 assert_eq!(index_instrument_spx.id(), InstrumentId::from("SPX.INDEX"));
359 assert_eq!(index_instrument_spx.asset_class(), AssetClass::Index);
360 assert_eq!(
361 index_instrument_spx.instrument_class(),
362 InstrumentClass::Spot
363 );
364 assert_eq!(index_instrument_spx.quote_currency(), Currency::USD());
365 assert!(!index_instrument_spx.is_inverse());
366 assert_eq!(index_instrument_spx.price_precision(), 2);
367 assert_eq!(index_instrument_spx.size_precision(), 0);
368 }
369
370 #[rstest]
371 fn test_new_checked_price_precision_mismatch() {
372 let result = IndexInstrument::new_checked(
373 InstrumentId::from("SPX.INDEX"),
374 Symbol::from("SPX"),
375 Currency::USD(),
376 4, 0,
378 Price::from("0.01"),
379 Quantity::from("1"),
380 None,
381 None,
382 0.into(),
383 0.into(),
384 );
385 assert!(result.is_err());
386 }
387
388 #[rstest]
389 fn test_serialization_roundtrip(index_instrument_spx: IndexInstrument) {
390 let json = serde_json::to_string(&index_instrument_spx).unwrap();
391 let deserialized: IndexInstrument = serde_json::from_str(&json).unwrap();
392 assert_eq!(index_instrument_spx, deserialized);
393 }
394
395 #[rstest]
396 fn test_builder_matches_new_checked() {
397 let positional = IndexInstrument::new_checked(
398 InstrumentId::from("SPX.INDEX"),
399 Symbol::from("SPX"),
400 Currency::USD(),
401 2,
402 0,
403 Price::from("0.01"),
404 Quantity::from("1"),
405 None,
406 None,
407 1.into(),
408 2.into(),
409 )
410 .unwrap();
411
412 let built = IndexInstrument::builder()
413 .instrument_id(InstrumentId::from("SPX.INDEX"))
414 .raw_symbol(Symbol::from("SPX"))
415 .currency(Currency::USD())
416 .price_precision(2)
417 .size_precision(0)
418 .price_increment(Price::from("0.01"))
419 .size_increment(Quantity::from("1"))
420 .ts_event(1.into())
421 .ts_init(2.into())
422 .build()
423 .unwrap();
424
425 assert_eq!(
426 serde_json::to_value(&positional).unwrap(),
427 serde_json::to_value(&built).unwrap(),
428 );
429 }
430}