Skip to main content

nautilus_model/instruments/
index_instrument.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Represents a generic index instrument.
38///
39/// An index is typically not directly tradable.
40#[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    /// The instrument ID.
52    pub id: InstrumentId,
53    /// The raw/local/native symbol for the instrument, assigned by the venue.
54    pub raw_symbol: Symbol,
55    /// The index currency.
56    pub currency: Currency,
57    /// The price decimal precision.
58    pub price_precision: u8,
59    /// The trading size decimal precision.
60    pub size_precision: u8,
61    /// The minimum price increment (tick size).
62    pub price_increment: Price,
63    /// The minimum size increment.
64    pub size_increment: Quantity,
65    /// The registered variable tick scheme name.
66    pub tick_scheme: Option<Ustr>,
67    /// Additional instrument metadata as a JSON-serializable dictionary.
68    pub info: Option<Params>,
69    /// UNIX timestamp (nanoseconds) when the data event occurred.
70    pub ts_event: UnixNanos,
71    /// UNIX timestamp (nanoseconds) when the data object was initialized.
72    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    /// Returns a fluent builder for a [`IndexInstrument`] instance.
123    ///
124    /// Required fields are enforced at compile time; optional fields can be omitted and use the
125    /// same defaults as checked construction. The same correctness checks run on `build`.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if any input validation fails.
130    #[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, // mismatch
331            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}