Skip to main content

nautilus_model/instruments/
commodity.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 rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny};
27use crate::{
28    enums::{AssetClass, InstrumentClass, OptionKind},
29    identifiers::{InstrumentId, Symbol},
30    types::{
31        currency::Currency,
32        money::Money,
33        price::{Price, check_positive_price},
34        quantity::{Quantity, check_positive_quantity},
35    },
36};
37
38/// Represents a generic commodity instrument.
39#[repr(C)]
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
44)]
45#[cfg_attr(
46    feature = "python",
47    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
48)]
49pub struct Commodity {
50    /// The instrument ID.
51    pub id: InstrumentId,
52    /// The raw/local/native symbol for the instrument, assigned by the venue.
53    pub raw_symbol: Symbol,
54    /// The asset class of the commodity.
55    pub asset_class: AssetClass,
56    /// The quote currency.
57    pub quote_currency: Currency,
58    /// The price decimal precision.
59    pub price_precision: u8,
60    /// The trading size decimal precision.
61    pub size_precision: u8,
62    /// The minimum price increment (tick size).
63    pub price_increment: Price,
64    /// The minimum size increment.
65    pub size_increment: Quantity,
66    /// The initial (order) margin requirement in percentage of order value.
67    pub margin_init: Decimal,
68    /// The maintenance (position) margin in percentage of position value.
69    pub margin_maint: Decimal,
70    /// The fee rate for liquidity makers as a percentage of order value.
71    pub maker_fee: Decimal,
72    /// The fee rate for liquidity takers as a percentage of order value.
73    pub taker_fee: Decimal,
74    /// The rounded lot unit size (standard/board).
75    pub lot_size: Option<Quantity>,
76    /// The maximum allowable order quantity.
77    pub max_quantity: Option<Quantity>,
78    /// The minimum allowable order quantity.
79    pub min_quantity: Option<Quantity>,
80    /// The maximum allowable order notional value.
81    pub max_notional: Option<Money>,
82    /// The minimum allowable order notional value.
83    pub min_notional: Option<Money>,
84    /// The maximum allowable quoted price.
85    pub max_price: Option<Price>,
86    /// The minimum allowable quoted price.
87    pub min_price: Option<Price>,
88    /// Additional instrument metadata as a JSON-serializable dictionary.
89    pub info: Option<Params>,
90    /// UNIX timestamp (nanoseconds) when the data event occurred.
91    pub ts_event: UnixNanos,
92    /// UNIX timestamp (nanoseconds) when the data object was initialized.
93    pub ts_init: UnixNanos,
94}
95
96impl Commodity {
97    /// Creates a new [`Commodity`] instance with correctness checking.
98    ///
99    /// # Notes
100    ///
101    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
102    /// # Errors
103    ///
104    /// Returns an error if any input validation fails.
105    #[expect(clippy::too_many_arguments)]
106    pub fn new_checked(
107        instrument_id: InstrumentId,
108        raw_symbol: Symbol,
109        asset_class: AssetClass,
110        quote_currency: Currency,
111        price_precision: u8,
112        size_precision: u8,
113        price_increment: Price,
114        size_increment: Quantity,
115        lot_size: Option<Quantity>,
116        max_quantity: Option<Quantity>,
117        min_quantity: Option<Quantity>,
118        max_notional: Option<Money>,
119        min_notional: Option<Money>,
120        max_price: Option<Price>,
121        min_price: Option<Price>,
122        margin_init: Option<Decimal>,
123        margin_maint: Option<Decimal>,
124        maker_fee: Option<Decimal>,
125        taker_fee: Option<Decimal>,
126        info: Option<Params>,
127        ts_event: UnixNanos,
128        ts_init: UnixNanos,
129    ) -> CorrectnessResult<Self> {
130        check_equal_u8(
131            price_precision,
132            price_increment.precision,
133            stringify!(price_precision),
134            stringify!(price_increment.precision),
135        )?;
136        check_equal_u8(
137            size_precision,
138            size_increment.precision,
139            stringify!(size_precision),
140            stringify!(size_increment.precision),
141        )?;
142        check_positive_price(price_increment, stringify!(price_increment))?;
143        check_positive_quantity(size_increment, stringify!(size_increment))?;
144
145        Ok(Self {
146            id: instrument_id,
147            raw_symbol,
148            asset_class,
149            quote_currency,
150            price_precision,
151            size_precision,
152            price_increment,
153            size_increment,
154            lot_size,
155            max_quantity,
156            min_quantity,
157            max_notional,
158            min_notional,
159            max_price,
160            min_price,
161            margin_init: margin_init.unwrap_or_default(),
162            margin_maint: margin_maint.unwrap_or_default(),
163            maker_fee: maker_fee.unwrap_or_default(),
164            taker_fee: taker_fee.unwrap_or_default(),
165            info,
166            ts_event,
167            ts_init,
168        })
169    }
170
171    /// Creates a new [`Commodity`] instance.
172    ///
173    /// # Panics
174    ///
175    /// Panics if any parameter is invalid (see `new_checked`).
176    #[expect(clippy::too_many_arguments)]
177    #[must_use]
178    pub fn new(
179        instrument_id: InstrumentId,
180        raw_symbol: Symbol,
181        asset_class: AssetClass,
182        quote_currency: Currency,
183        price_precision: u8,
184        size_precision: u8,
185        price_increment: Price,
186        size_increment: Quantity,
187        lot_size: Option<Quantity>,
188        max_quantity: Option<Quantity>,
189        min_quantity: Option<Quantity>,
190        max_notional: Option<Money>,
191        min_notional: Option<Money>,
192        max_price: Option<Price>,
193        min_price: Option<Price>,
194        margin_init: Option<Decimal>,
195        margin_maint: Option<Decimal>,
196        maker_fee: Option<Decimal>,
197        taker_fee: Option<Decimal>,
198        info: Option<Params>,
199        ts_event: UnixNanos,
200        ts_init: UnixNanos,
201    ) -> Self {
202        Self::new_checked(
203            instrument_id,
204            raw_symbol,
205            asset_class,
206            quote_currency,
207            price_precision,
208            size_precision,
209            price_increment,
210            size_increment,
211            lot_size,
212            max_quantity,
213            min_quantity,
214            max_notional,
215            min_notional,
216            max_price,
217            min_price,
218            margin_init,
219            margin_maint,
220            maker_fee,
221            taker_fee,
222            info,
223            ts_event,
224            ts_init,
225        )
226        .expect_display(FAILED)
227    }
228}
229
230impl PartialEq<Self> for Commodity {
231    fn eq(&self, other: &Self) -> bool {
232        self.id == other.id
233    }
234}
235
236impl Eq for Commodity {}
237
238impl Hash for Commodity {
239    fn hash<H: Hasher>(&self, state: &mut H) {
240        self.id.hash(state);
241    }
242}
243
244impl Instrument for Commodity {
245    fn into_any(self) -> InstrumentAny {
246        InstrumentAny::Commodity(self)
247    }
248
249    fn id(&self) -> InstrumentId {
250        self.id
251    }
252
253    fn raw_symbol(&self) -> Symbol {
254        self.raw_symbol
255    }
256
257    fn asset_class(&self) -> AssetClass {
258        self.asset_class
259    }
260
261    fn instrument_class(&self) -> InstrumentClass {
262        InstrumentClass::Spot
263    }
264
265    fn underlying(&self) -> Option<Ustr> {
266        None
267    }
268
269    fn base_currency(&self) -> Option<Currency> {
270        None
271    }
272
273    fn quote_currency(&self) -> Currency {
274        self.quote_currency
275    }
276
277    fn settlement_currency(&self) -> Currency {
278        self.quote_currency
279    }
280
281    fn isin(&self) -> Option<Ustr> {
282        None
283    }
284
285    fn option_kind(&self) -> Option<OptionKind> {
286        None
287    }
288
289    fn exchange(&self) -> Option<Ustr> {
290        None
291    }
292
293    fn strike_price(&self) -> Option<Price> {
294        None
295    }
296
297    fn activation_ns(&self) -> Option<UnixNanos> {
298        None
299    }
300
301    fn expiration_ns(&self) -> Option<UnixNanos> {
302        None
303    }
304
305    fn is_inverse(&self) -> bool {
306        false
307    }
308
309    fn price_precision(&self) -> u8 {
310        self.price_precision
311    }
312
313    fn size_precision(&self) -> u8 {
314        self.size_precision
315    }
316
317    fn price_increment(&self) -> Price {
318        self.price_increment
319    }
320
321    fn size_increment(&self) -> Quantity {
322        self.size_increment
323    }
324
325    fn multiplier(&self) -> Quantity {
326        Quantity::from(1)
327    }
328
329    fn lot_size(&self) -> Option<Quantity> {
330        self.lot_size
331    }
332
333    fn max_quantity(&self) -> Option<Quantity> {
334        self.max_quantity
335    }
336
337    fn min_quantity(&self) -> Option<Quantity> {
338        self.min_quantity
339    }
340
341    fn max_notional(&self) -> Option<Money> {
342        self.max_notional
343    }
344
345    fn min_notional(&self) -> Option<Money> {
346        self.min_notional
347    }
348
349    fn max_price(&self) -> Option<Price> {
350        self.max_price
351    }
352
353    fn min_price(&self) -> Option<Price> {
354        self.min_price
355    }
356
357    fn margin_init(&self) -> Decimal {
358        self.margin_init
359    }
360
361    fn margin_maint(&self) -> Decimal {
362        self.margin_maint
363    }
364
365    fn maker_fee(&self) -> Decimal {
366        self.maker_fee
367    }
368
369    fn taker_fee(&self) -> Decimal {
370        self.taker_fee
371    }
372
373    fn ts_event(&self) -> UnixNanos {
374        self.ts_event
375    }
376
377    fn ts_init(&self) -> UnixNanos {
378        self.ts_init
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use rstest::rstest;
385
386    use crate::{
387        enums::{AssetClass, InstrumentClass},
388        identifiers::{InstrumentId, Symbol},
389        instruments::{Commodity, Instrument, stubs::*},
390        types::{Currency, Price, Quantity},
391    };
392
393    #[rstest]
394    fn test_trait_accessors(commodity_gold: Commodity) {
395        assert_eq!(commodity_gold.id(), InstrumentId::from("GOLD.COMEX"));
396        assert_eq!(commodity_gold.asset_class(), AssetClass::Commodity);
397        assert_eq!(commodity_gold.instrument_class(), InstrumentClass::Spot);
398        assert_eq!(commodity_gold.quote_currency(), Currency::USD());
399        assert!(!commodity_gold.is_inverse());
400        assert_eq!(commodity_gold.price_precision(), 2);
401        assert_eq!(commodity_gold.size_precision(), 0);
402    }
403
404    #[rstest]
405    fn test_new_checked_price_precision_mismatch() {
406        let result = Commodity::new_checked(
407            InstrumentId::from("TEST.COMEX"),
408            Symbol::from("TEST"),
409            AssetClass::Commodity,
410            Currency::USD(),
411            4, // mismatch
412            0,
413            Price::from("0.01"),
414            Quantity::from("1"),
415            None,
416            None,
417            None,
418            None,
419            None,
420            None,
421            None,
422            None,
423            None,
424            None,
425            None,
426            None,
427            0.into(),
428            0.into(),
429        );
430        assert!(result.is_err());
431    }
432
433    #[rstest]
434    fn test_serialization_roundtrip(commodity_gold: Commodity) {
435        let json = serde_json::to_string(&commodity_gold).unwrap();
436        let deserialized: Commodity = serde_json::from_str(&json).unwrap();
437        assert_eq!(commodity_gold, deserialized);
438    }
439}