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, CorrectnessResultExt, FAILED, check_equal_u8},
21};
22use serde::{Deserialize, Serialize};
23use ustr::Ustr;
24
25use super::{Instrument, any::InstrumentAny};
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.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    /// 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    /// Additional instrument metadata as a JSON-serializable dictionary.
66    pub info: Option<Params>,
67    /// UNIX timestamp (nanoseconds) when the data event occurred.
68    pub ts_event: UnixNanos,
69    /// UNIX timestamp (nanoseconds) when the data object was initialized.
70    pub ts_init: UnixNanos,
71}
72
73impl IndexInstrument {
74    /// Creates a new [`IndexInstrument`] instance with correctness checking.
75    ///
76    /// # Notes
77    ///
78    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
79    /// # Errors
80    ///
81    /// Returns an error if any input validation fails.
82    #[expect(clippy::too_many_arguments)]
83    pub fn new_checked(
84        instrument_id: InstrumentId,
85        raw_symbol: Symbol,
86        currency: Currency,
87        price_precision: u8,
88        size_precision: u8,
89        price_increment: Price,
90        size_increment: Quantity,
91        info: Option<Params>,
92        ts_event: UnixNanos,
93        ts_init: UnixNanos,
94    ) -> CorrectnessResult<Self> {
95        check_equal_u8(
96            price_precision,
97            price_increment.precision,
98            stringify!(price_precision),
99            stringify!(price_increment.precision),
100        )?;
101        check_equal_u8(
102            size_precision,
103            size_increment.precision,
104            stringify!(size_precision),
105            stringify!(size_increment.precision),
106        )?;
107        check_positive_price(price_increment, stringify!(price_increment))?;
108        check_positive_quantity(size_increment, stringify!(size_increment))?;
109
110        Ok(Self {
111            id: instrument_id,
112            raw_symbol,
113            currency,
114            price_precision,
115            size_precision,
116            price_increment,
117            size_increment,
118            info,
119            ts_event,
120            ts_init,
121        })
122    }
123
124    /// Creates a new [`IndexInstrument`] instance.
125    ///
126    /// # Panics
127    ///
128    /// Panics if any parameter is invalid (see `new_checked`).
129    #[expect(clippy::too_many_arguments)]
130    #[must_use]
131    pub fn new(
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        info: Option<Params>,
140        ts_event: UnixNanos,
141        ts_init: UnixNanos,
142    ) -> Self {
143        Self::new_checked(
144            instrument_id,
145            raw_symbol,
146            currency,
147            price_precision,
148            size_precision,
149            price_increment,
150            size_increment,
151            info,
152            ts_event,
153            ts_init,
154        )
155        .expect_display(FAILED)
156    }
157}
158
159impl PartialEq<Self> for IndexInstrument {
160    fn eq(&self, other: &Self) -> bool {
161        self.id == other.id
162    }
163}
164
165impl Eq for IndexInstrument {}
166
167impl Hash for IndexInstrument {
168    fn hash<H: Hasher>(&self, state: &mut H) {
169        self.id.hash(state);
170    }
171}
172
173impl Instrument for IndexInstrument {
174    fn into_any(self) -> InstrumentAny {
175        InstrumentAny::IndexInstrument(self)
176    }
177
178    fn id(&self) -> InstrumentId {
179        self.id
180    }
181
182    fn raw_symbol(&self) -> Symbol {
183        self.raw_symbol
184    }
185
186    fn asset_class(&self) -> AssetClass {
187        AssetClass::Index
188    }
189
190    fn instrument_class(&self) -> InstrumentClass {
191        InstrumentClass::Spot
192    }
193
194    fn underlying(&self) -> Option<Ustr> {
195        None
196    }
197
198    fn base_currency(&self) -> Option<Currency> {
199        None
200    }
201
202    fn quote_currency(&self) -> Currency {
203        self.currency
204    }
205
206    fn settlement_currency(&self) -> Currency {
207        self.currency
208    }
209
210    fn isin(&self) -> Option<Ustr> {
211        None
212    }
213
214    fn option_kind(&self) -> Option<OptionKind> {
215        None
216    }
217
218    fn exchange(&self) -> Option<Ustr> {
219        None
220    }
221
222    fn strike_price(&self) -> Option<Price> {
223        None
224    }
225
226    fn activation_ns(&self) -> Option<UnixNanos> {
227        None
228    }
229
230    fn expiration_ns(&self) -> Option<UnixNanos> {
231        None
232    }
233
234    fn is_inverse(&self) -> bool {
235        false
236    }
237
238    fn price_precision(&self) -> u8 {
239        self.price_precision
240    }
241
242    fn size_precision(&self) -> u8 {
243        self.size_precision
244    }
245
246    fn price_increment(&self) -> Price {
247        self.price_increment
248    }
249
250    fn size_increment(&self) -> Quantity {
251        self.size_increment
252    }
253
254    fn multiplier(&self) -> Quantity {
255        Quantity::from(1)
256    }
257
258    fn lot_size(&self) -> Option<Quantity> {
259        None
260    }
261
262    fn max_quantity(&self) -> Option<Quantity> {
263        None
264    }
265
266    fn min_quantity(&self) -> Option<Quantity> {
267        None
268    }
269
270    fn max_notional(&self) -> Option<Money> {
271        None
272    }
273
274    fn min_notional(&self) -> Option<Money> {
275        None
276    }
277
278    fn max_price(&self) -> Option<Price> {
279        None
280    }
281
282    fn min_price(&self) -> Option<Price> {
283        None
284    }
285
286    fn ts_event(&self) -> UnixNanos {
287        self.ts_event
288    }
289
290    fn ts_init(&self) -> UnixNanos {
291        self.ts_init
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use rstest::rstest;
298
299    use crate::{
300        enums::{AssetClass, InstrumentClass},
301        identifiers::{InstrumentId, Symbol},
302        instruments::{IndexInstrument, Instrument, stubs::*},
303        types::{Currency, Price, Quantity},
304    };
305
306    #[rstest]
307    fn test_trait_accessors(index_instrument_spx: IndexInstrument) {
308        assert_eq!(index_instrument_spx.id(), InstrumentId::from("SPX.INDEX"));
309        assert_eq!(index_instrument_spx.asset_class(), AssetClass::Index);
310        assert_eq!(
311            index_instrument_spx.instrument_class(),
312            InstrumentClass::Spot
313        );
314        assert_eq!(index_instrument_spx.quote_currency(), Currency::USD());
315        assert!(!index_instrument_spx.is_inverse());
316        assert_eq!(index_instrument_spx.price_precision(), 2);
317        assert_eq!(index_instrument_spx.size_precision(), 0);
318    }
319
320    #[rstest]
321    fn test_new_checked_price_precision_mismatch() {
322        let result = IndexInstrument::new_checked(
323            InstrumentId::from("SPX.INDEX"),
324            Symbol::from("SPX"),
325            Currency::USD(),
326            4, // mismatch
327            0,
328            Price::from("0.01"),
329            Quantity::from("1"),
330            None,
331            0.into(),
332            0.into(),
333        );
334        assert!(result.is_err());
335    }
336
337    #[rstest]
338    fn test_serialization_roundtrip(index_instrument_spx: IndexInstrument) {
339        let json = serde_json::to_string(&index_instrument_spx).unwrap();
340        let deserialized: IndexInstrument = serde_json::from_str(&json).unwrap();
341        assert_eq!(index_instrument_spx, deserialized);
342    }
343}