nautilus_model/instruments/
index_instrument.rs1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{CorrectnessResult, 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.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)]
78 fn new_checked(
79 instrument_id: InstrumentId,
80 raw_symbol: Symbol,
81 currency: Currency,
82 price_precision: u8,
83 size_precision: u8,
84 price_increment: Price,
85 size_increment: Quantity,
86 tick_scheme: Option<Ustr>,
87 info: Option<Params>,
88 ts_event: UnixNanos,
89 ts_init: UnixNanos,
90 ) -> CorrectnessResult<Self> {
91 check_equal_u8(
92 price_precision,
93 price_increment.precision,
94 stringify!(price_precision),
95 stringify!(price_increment.precision),
96 )?;
97 check_equal_u8(
98 size_precision,
99 size_increment.precision,
100 stringify!(size_precision),
101 stringify!(size_increment.precision),
102 )?;
103 check_positive_price(price_increment, stringify!(price_increment))?;
104 check_positive_quantity(size_increment, stringify!(size_increment))?;
105 check_tick_scheme(tick_scheme)?;
106
107 Ok(Self {
108 id: instrument_id,
109 raw_symbol,
110 currency,
111 price_precision,
112 size_precision,
113 price_increment,
114 size_increment,
115 tick_scheme,
116 info,
117 ts_event,
118 ts_init,
119 })
120 }
121
122 #[builder(start_fn = builder, finish_fn = build)]
131 pub fn build_checked(
132 instrument_id: InstrumentId,
133 raw_symbol: Symbol,
134 currency: Currency,
135 price_precision: u8,
136 size_precision: u8,
137 price_increment: Price,
138 size_increment: Quantity,
139 tick_scheme: Option<Ustr>,
140 info: Option<Params>,
141 ts_event: UnixNanos,
142 ts_init: UnixNanos,
143 ) -> CorrectnessResult<Self> {
144 Self::new_checked(
145 instrument_id,
146 raw_symbol,
147 currency,
148 price_precision,
149 size_precision,
150 price_increment,
151 size_increment,
152 tick_scheme,
153 info,
154 ts_event,
155 ts_init,
156 )
157 }
158}
159
160impl PartialEq<Self> for IndexInstrument {
161 fn eq(&self, other: &Self) -> bool {
162 self.id == other.id
163 }
164}
165
166impl Eq for IndexInstrument {}
167
168impl Hash for IndexInstrument {
169 fn hash<H: Hasher>(&self, state: &mut H) {
170 self.id.hash(state);
171 }
172}
173
174impl Instrument for IndexInstrument {
175 fn tick_scheme(&self) -> Option<Ustr> {
176 self.tick_scheme
177 }
178 fn into_any(self) -> InstrumentAny {
179 InstrumentAny::IndexInstrument(self)
180 }
181
182 fn id(&self) -> InstrumentId {
183 self.id
184 }
185
186 fn raw_symbol(&self) -> Symbol {
187 self.raw_symbol
188 }
189
190 fn asset_class(&self) -> AssetClass {
191 AssetClass::Index
192 }
193
194 fn instrument_class(&self) -> InstrumentClass {
195 InstrumentClass::Spot
196 }
197
198 fn underlying(&self) -> Option<Ustr> {
199 None
200 }
201
202 fn base_currency(&self) -> Option<Currency> {
203 None
204 }
205
206 fn quote_currency(&self) -> Currency {
207 self.currency
208 }
209
210 fn settlement_currency(&self) -> Currency {
211 self.currency
212 }
213
214 fn isin(&self) -> Option<Ustr> {
215 None
216 }
217
218 fn option_kind(&self) -> Option<OptionKind> {
219 None
220 }
221
222 fn exchange(&self) -> Option<Ustr> {
223 None
224 }
225
226 fn strike_price(&self) -> Option<Price> {
227 None
228 }
229
230 fn activation_ns(&self) -> Option<UnixNanos> {
231 None
232 }
233
234 fn expiration_ns(&self) -> Option<UnixNanos> {
235 None
236 }
237
238 fn is_inverse(&self) -> bool {
239 false
240 }
241
242 fn price_precision(&self) -> u8 {
243 self.price_precision
244 }
245
246 fn size_precision(&self) -> u8 {
247 self.size_precision
248 }
249
250 fn price_increment(&self) -> Price {
251 self.price_increment
252 }
253
254 fn size_increment(&self) -> Quantity {
255 self.size_increment
256 }
257
258 fn multiplier(&self) -> Quantity {
259 Quantity::from(1)
260 }
261
262 fn lot_size(&self) -> Option<Quantity> {
263 None
264 }
265
266 fn max_quantity(&self) -> Option<Quantity> {
267 None
268 }
269
270 fn min_quantity(&self) -> Option<Quantity> {
271 None
272 }
273
274 fn max_notional(&self) -> Option<Money> {
275 None
276 }
277
278 fn min_notional(&self) -> Option<Money> {
279 None
280 }
281
282 fn max_price(&self) -> Option<Price> {
283 None
284 }
285
286 fn min_price(&self) -> Option<Price> {
287 None
288 }
289
290 fn ts_event(&self) -> UnixNanos {
291 self.ts_event
292 }
293
294 fn ts_init(&self) -> UnixNanos {
295 self.ts_init
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use rstest::rstest;
302
303 use crate::{
304 enums::{AssetClass, InstrumentClass},
305 identifiers::{InstrumentId, Symbol},
306 instruments::{IndexInstrument, Instrument, stubs::*},
307 types::{Currency, Price, Quantity},
308 };
309
310 #[rstest]
311 fn test_trait_accessors(index_instrument_spx: IndexInstrument) {
312 assert_eq!(index_instrument_spx.id(), InstrumentId::from("SPX.INDEX"));
313 assert_eq!(index_instrument_spx.asset_class(), AssetClass::Index);
314 assert_eq!(
315 index_instrument_spx.instrument_class(),
316 InstrumentClass::Spot
317 );
318 assert_eq!(index_instrument_spx.quote_currency(), Currency::USD());
319 assert!(!index_instrument_spx.is_inverse());
320 assert_eq!(index_instrument_spx.price_precision(), 2);
321 assert_eq!(index_instrument_spx.size_precision(), 0);
322 }
323
324 #[rstest]
325 fn test_new_checked_price_precision_mismatch() {
326 let result = IndexInstrument::new_checked(
327 InstrumentId::from("SPX.INDEX"),
328 Symbol::from("SPX"),
329 Currency::USD(),
330 4, 0,
332 Price::from("0.01"),
333 Quantity::from("1"),
334 None,
335 None,
336 0.into(),
337 0.into(),
338 );
339 assert!(result.is_err());
340 }
341
342 #[rstest]
343 fn test_serialization_roundtrip(index_instrument_spx: IndexInstrument) {
344 let json = serde_json::to_string(&index_instrument_spx).unwrap();
345 let deserialized: IndexInstrument = serde_json::from_str(&json).unwrap();
346 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
347 }
348
349 #[rstest]
350 fn test_builder_matches_new_checked() {
351 let positional = IndexInstrument::new_checked(
352 InstrumentId::from("SPX.INDEX"),
353 Symbol::from("SPX"),
354 Currency::USD(),
355 2,
356 0,
357 Price::from("0.01"),
358 Quantity::from("1"),
359 None,
360 None,
361 1.into(),
362 2.into(),
363 )
364 .unwrap();
365
366 let built = IndexInstrument::builder()
367 .instrument_id(InstrumentId::from("SPX.INDEX"))
368 .raw_symbol(Symbol::from("SPX"))
369 .currency(Currency::USD())
370 .price_precision(2)
371 .size_precision(0)
372 .price_increment(Price::from("0.01"))
373 .size_increment(Quantity::from("1"))
374 .ts_event(1.into())
375 .ts_init(2.into())
376 .build()
377 .unwrap();
378
379 assert_eq!(
380 serde_json::to_value(&positional).unwrap(),
381 serde_json::to_value(&built).unwrap(),
382 );
383 }
384}