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 into_any(self) -> InstrumentAny {
176 InstrumentAny::IndexInstrument(self)
177 }
178
179 fn id(&self) -> InstrumentId {
180 self.id
181 }
182
183 fn raw_symbol(&self) -> Symbol {
184 self.raw_symbol
185 }
186
187 fn asset_class(&self) -> AssetClass {
188 AssetClass::Index
189 }
190
191 fn instrument_class(&self) -> InstrumentClass {
192 InstrumentClass::Spot
193 }
194
195 fn underlying(&self) -> Option<Ustr> {
196 None
197 }
198
199 fn base_currency(&self) -> Option<Currency> {
200 None
201 }
202
203 fn quote_currency(&self) -> Currency {
204 self.currency
205 }
206
207 fn settlement_currency(&self) -> Currency {
208 self.currency
209 }
210
211 fn isin(&self) -> Option<Ustr> {
212 None
213 }
214
215 fn option_kind(&self) -> Option<OptionKind> {
216 None
217 }
218
219 fn exchange(&self) -> Option<Ustr> {
220 None
221 }
222
223 fn strike_price(&self) -> Option<Price> {
224 None
225 }
226
227 fn activation_ns(&self) -> Option<UnixNanos> {
228 None
229 }
230
231 fn expiration_ns(&self) -> Option<UnixNanos> {
232 None
233 }
234
235 fn is_inverse(&self) -> bool {
236 false
237 }
238
239 fn price_precision(&self) -> u8 {
240 self.price_precision
241 }
242
243 fn size_precision(&self) -> u8 {
244 self.size_precision
245 }
246
247 fn price_increment(&self) -> Price {
248 self.price_increment
249 }
250
251 fn size_increment(&self) -> Quantity {
252 self.size_increment
253 }
254
255 fn multiplier(&self) -> Quantity {
256 Quantity::from(1)
257 }
258
259 fn lot_size(&self) -> Option<Quantity> {
260 None
261 }
262
263 fn max_quantity(&self) -> Option<Quantity> {
264 None
265 }
266
267 fn min_quantity(&self) -> Option<Quantity> {
268 None
269 }
270
271 fn max_notional(&self) -> Option<Money> {
272 None
273 }
274
275 fn min_notional(&self) -> Option<Money> {
276 None
277 }
278
279 fn max_price(&self) -> Option<Price> {
280 None
281 }
282
283 fn min_price(&self) -> Option<Price> {
284 None
285 }
286
287 fn tick_scheme(&self) -> Option<Ustr> {
288 self.tick_scheme
289 }
290
291 fn info(&self) -> Option<&Params> {
292 self.info.as_ref()
293 }
294
295 fn ts_event(&self) -> UnixNanos {
296 self.ts_event
297 }
298
299 fn ts_init(&self) -> UnixNanos {
300 self.ts_init
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use rstest::rstest;
307
308 use crate::{
309 enums::{AssetClass, InstrumentClass},
310 identifiers::{InstrumentId, Symbol},
311 instruments::{IndexInstrument, Instrument, stubs::*},
312 types::{Currency, Price, Quantity},
313 };
314
315 #[rstest]
316 fn test_trait_accessors(index_instrument_spx: IndexInstrument) {
317 assert_eq!(index_instrument_spx.id(), InstrumentId::from("SPX.INDEX"));
318 assert_eq!(index_instrument_spx.asset_class(), AssetClass::Index);
319 assert_eq!(
320 index_instrument_spx.instrument_class(),
321 InstrumentClass::Spot
322 );
323 assert_eq!(index_instrument_spx.quote_currency(), Currency::USD());
324 assert!(!index_instrument_spx.is_inverse());
325 assert_eq!(index_instrument_spx.price_precision(), 2);
326 assert_eq!(index_instrument_spx.size_precision(), 0);
327 }
328
329 #[rstest]
330 fn test_new_checked_price_precision_mismatch() {
331 let result = IndexInstrument::new_checked(
332 InstrumentId::from("SPX.INDEX"),
333 Symbol::from("SPX"),
334 Currency::USD(),
335 4, 0,
337 Price::from("0.01"),
338 Quantity::from("1"),
339 None,
340 None,
341 0.into(),
342 0.into(),
343 );
344 assert!(result.is_err());
345 }
346
347 #[rstest]
348 fn test_serialization_roundtrip(index_instrument_spx: IndexInstrument) {
349 let json = serde_json::to_string(&index_instrument_spx).unwrap();
350 let deserialized: IndexInstrument = serde_json::from_str(&json).unwrap();
351 assert_eq!(json, serde_json::to_string(&deserialized).unwrap());
352 }
353
354 #[rstest]
355 fn test_builder_matches_new_checked() {
356 let positional = IndexInstrument::new_checked(
357 InstrumentId::from("SPX.INDEX"),
358 Symbol::from("SPX"),
359 Currency::USD(),
360 2,
361 0,
362 Price::from("0.01"),
363 Quantity::from("1"),
364 None,
365 None,
366 1.into(),
367 2.into(),
368 )
369 .unwrap();
370
371 let built = IndexInstrument::builder()
372 .instrument_id(InstrumentId::from("SPX.INDEX"))
373 .raw_symbol(Symbol::from("SPX"))
374 .currency(Currency::USD())
375 .price_precision(2)
376 .size_precision(0)
377 .price_increment(Price::from("0.01"))
378 .size_increment(Quantity::from("1"))
379 .ts_event(1.into())
380 .ts_init(2.into())
381 .build()
382 .unwrap();
383
384 assert_eq!(
385 serde_json::to_value(&positional).unwrap(),
386 serde_json::to_value(&built).unwrap(),
387 );
388 }
389}