1use 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#[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 pub id: InstrumentId,
52 pub raw_symbol: Symbol,
54 pub asset_class: AssetClass,
56 pub quote_currency: Currency,
58 pub price_precision: u8,
60 pub size_precision: u8,
62 pub price_increment: Price,
64 pub size_increment: Quantity,
66 pub margin_init: Decimal,
68 pub margin_maint: Decimal,
70 pub maker_fee: Decimal,
72 pub taker_fee: Decimal,
74 pub lot_size: Option<Quantity>,
76 pub max_quantity: Option<Quantity>,
78 pub min_quantity: Option<Quantity>,
80 pub max_notional: Option<Money>,
82 pub min_notional: Option<Money>,
84 pub max_price: Option<Price>,
86 pub min_price: Option<Price>,
88 pub info: Option<Params>,
90 pub ts_event: UnixNanos,
92 pub ts_init: UnixNanos,
94}
95
96impl Commodity {
97 #[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 #[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, 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}