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 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, // mismatch
336            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}