1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{
21 CorrectnessResult, CorrectnessResultExt, FAILED, check_equal_u8,
22 check_valid_string_ascii_optional,
23 },
24};
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use super::{Instrument, any::InstrumentAny};
30use crate::{
31 enums::{AssetClass, InstrumentClass, OptionKind},
32 identifiers::{InstrumentId, Symbol},
33 types::{
34 currency::Currency,
35 money::Money,
36 price::{Price, check_positive_price},
37 quantity::Quantity,
38 },
39};
40
41#[repr(C)]
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
51)]
52pub struct Equity {
53 pub id: InstrumentId,
55 pub raw_symbol: Symbol,
57 pub isin: Option<Ustr>,
59 pub currency: Currency,
61 pub price_precision: u8,
63 pub price_increment: Price,
65 pub margin_init: Decimal,
67 pub margin_maint: Decimal,
69 pub maker_fee: Decimal,
71 pub taker_fee: Decimal,
73 pub lot_size: Option<Quantity>,
75 pub max_quantity: Option<Quantity>,
77 pub min_quantity: Option<Quantity>,
79 pub max_price: Option<Price>,
81 pub min_price: Option<Price>,
83 pub info: Option<Params>,
85 pub ts_event: UnixNanos,
87 pub ts_init: UnixNanos,
89}
90
91impl Equity {
92 #[expect(clippy::too_many_arguments)]
101 pub fn new_checked(
102 instrument_id: InstrumentId,
103 raw_symbol: Symbol,
104 isin: Option<Ustr>,
105 currency: Currency,
106 price_precision: u8,
107 price_increment: Price,
108 lot_size: Option<Quantity>,
109 max_quantity: Option<Quantity>,
110 min_quantity: Option<Quantity>,
111 max_price: Option<Price>,
112 min_price: Option<Price>,
113 margin_init: Option<Decimal>,
114 margin_maint: Option<Decimal>,
115 maker_fee: Option<Decimal>,
116 taker_fee: Option<Decimal>,
117 info: Option<Params>,
118 ts_event: UnixNanos,
119 ts_init: UnixNanos,
120 ) -> CorrectnessResult<Self> {
121 check_valid_string_ascii_optional(isin.map(|u| u.as_str()), stringify!(isin))?;
122 check_equal_u8(
123 price_precision,
124 price_increment.precision,
125 stringify!(price_precision),
126 stringify!(price_increment.precision),
127 )?;
128 check_positive_price(price_increment, stringify!(price_increment))?;
129
130 Ok(Self {
131 id: instrument_id,
132 raw_symbol,
133 isin,
134 currency,
135 price_precision,
136 price_increment,
137 lot_size,
138 max_quantity,
139 min_quantity,
140 max_price,
141 min_price,
142 margin_init: margin_init.unwrap_or_default(),
143 margin_maint: margin_maint.unwrap_or_default(),
144 maker_fee: maker_fee.unwrap_or_default(),
145 taker_fee: taker_fee.unwrap_or_default(),
146 info,
147 ts_event,
148 ts_init,
149 })
150 }
151
152 #[expect(clippy::too_many_arguments)]
158 #[must_use]
159 pub fn new(
160 instrument_id: InstrumentId,
161 raw_symbol: Symbol,
162 isin: Option<Ustr>,
163 currency: Currency,
164 price_precision: u8,
165 price_increment: Price,
166 lot_size: Option<Quantity>,
167 max_quantity: Option<Quantity>,
168 min_quantity: Option<Quantity>,
169 max_price: Option<Price>,
170 min_price: Option<Price>,
171 margin_init: Option<Decimal>,
172 margin_maint: Option<Decimal>,
173 maker_fee: Option<Decimal>,
174 taker_fee: Option<Decimal>,
175 info: Option<Params>,
176 ts_event: UnixNanos,
177 ts_init: UnixNanos,
178 ) -> Self {
179 Self::new_checked(
180 instrument_id,
181 raw_symbol,
182 isin,
183 currency,
184 price_precision,
185 price_increment,
186 lot_size,
187 max_quantity,
188 min_quantity,
189 max_price,
190 min_price,
191 margin_init,
192 margin_maint,
193 maker_fee,
194 taker_fee,
195 info,
196 ts_event,
197 ts_init,
198 )
199 .expect_display(FAILED)
200 }
201}
202
203impl PartialEq<Self> for Equity {
204 fn eq(&self, other: &Self) -> bool {
205 self.id == other.id
206 }
207}
208
209impl Eq for Equity {}
210
211impl Hash for Equity {
212 fn hash<H: Hasher>(&self, state: &mut H) {
213 self.id.hash(state);
214 }
215}
216
217impl Instrument for Equity {
218 fn into_any(self) -> InstrumentAny {
219 InstrumentAny::Equity(self)
220 }
221
222 fn id(&self) -> InstrumentId {
223 self.id
224 }
225
226 fn raw_symbol(&self) -> Symbol {
227 self.raw_symbol
228 }
229
230 fn asset_class(&self) -> AssetClass {
231 AssetClass::Equity
232 }
233
234 fn instrument_class(&self) -> InstrumentClass {
235 InstrumentClass::Spot
236 }
237 fn underlying(&self) -> Option<Ustr> {
238 None
239 }
240
241 fn base_currency(&self) -> Option<Currency> {
242 None
243 }
244
245 fn quote_currency(&self) -> Currency {
246 self.currency
247 }
248
249 fn settlement_currency(&self) -> Currency {
250 self.currency
251 }
252
253 fn isin(&self) -> Option<Ustr> {
254 self.isin
255 }
256
257 fn option_kind(&self) -> Option<OptionKind> {
258 None
259 }
260
261 fn exchange(&self) -> Option<Ustr> {
262 None
263 }
264
265 fn strike_price(&self) -> Option<Price> {
266 None
267 }
268
269 fn activation_ns(&self) -> Option<UnixNanos> {
270 None
271 }
272
273 fn expiration_ns(&self) -> Option<UnixNanos> {
274 None
275 }
276
277 fn is_inverse(&self) -> bool {
278 false
279 }
280
281 fn price_precision(&self) -> u8 {
282 self.price_precision
283 }
284
285 fn size_precision(&self) -> u8 {
286 0
287 }
288
289 fn price_increment(&self) -> Price {
290 self.price_increment
291 }
292
293 fn size_increment(&self) -> Quantity {
294 Quantity::from(1)
295 }
296
297 fn multiplier(&self) -> Quantity {
298 Quantity::from(1)
299 }
300
301 fn lot_size(&self) -> Option<Quantity> {
302 self.lot_size
303 }
304
305 fn max_quantity(&self) -> Option<Quantity> {
306 self.max_quantity
307 }
308
309 fn min_quantity(&self) -> Option<Quantity> {
310 self.min_quantity
311 }
312
313 fn max_notional(&self) -> Option<Money> {
314 None
315 }
316
317 fn min_notional(&self) -> Option<Money> {
318 None
319 }
320
321 fn max_price(&self) -> Option<Price> {
322 self.max_price
323 }
324
325 fn min_price(&self) -> Option<Price> {
326 self.min_price
327 }
328
329 fn ts_event(&self) -> UnixNanos {
330 self.ts_event
331 }
332
333 fn ts_init(&self) -> UnixNanos {
334 self.ts_init
335 }
336
337 fn margin_init(&self) -> Decimal {
338 self.margin_init
339 }
340
341 fn margin_maint(&self) -> Decimal {
342 self.margin_maint
343 }
344
345 fn maker_fee(&self) -> Decimal {
346 self.maker_fee
347 }
348
349 fn taker_fee(&self) -> Decimal {
350 self.taker_fee
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use rstest::rstest;
357
358 use crate::{
359 enums::{AssetClass, InstrumentClass},
360 identifiers::{InstrumentId, Symbol},
361 instruments::{Equity, Instrument, stubs::*},
362 types::{Currency, Price, Quantity},
363 };
364
365 #[rstest]
366 fn test_trait_accessors(equity_aapl: Equity) {
367 assert_eq!(equity_aapl.id(), InstrumentId::from("AAPL.XNAS"));
368 assert_eq!(equity_aapl.raw_symbol(), Symbol::from("AAPL"));
369 assert_eq!(equity_aapl.asset_class(), AssetClass::Equity);
370 assert_eq!(equity_aapl.instrument_class(), InstrumentClass::Spot);
371 assert_eq!(equity_aapl.quote_currency(), Currency::USD());
372 assert_eq!(equity_aapl.settlement_currency(), Currency::USD());
373 assert!(!equity_aapl.is_inverse());
374 assert_eq!(equity_aapl.price_precision(), 2);
375 assert_eq!(equity_aapl.size_precision(), 0);
376 assert_eq!(equity_aapl.price_increment(), Price::from("0.01"));
377 assert_eq!(equity_aapl.size_increment(), Quantity::from("1"));
378 assert_eq!(equity_aapl.multiplier(), Quantity::from("1"));
379 assert_eq!(equity_aapl.base_currency(), None);
380 assert_eq!(equity_aapl.underlying(), None);
381 assert_eq!(equity_aapl.option_kind(), None);
382 assert_eq!(equity_aapl.strike_price(), None);
383 assert_eq!(equity_aapl.activation_ns(), None);
384 assert_eq!(equity_aapl.expiration_ns(), None);
385 }
386
387 #[rstest]
388 fn test_isin(equity_aapl: Equity) {
389 assert_eq!(
390 equity_aapl.isin().map(|u| u.to_string()),
391 Some("US0378331005".to_string()),
392 );
393 }
394
395 #[rstest]
396 fn test_new_checked_price_precision_mismatch() {
397 let result = Equity::new_checked(
398 InstrumentId::from("AAPL.XNAS"),
399 Symbol::from("AAPL"),
400 None,
401 Currency::USD(),
402 3, Price::from("0.01"),
404 None,
405 None,
406 None,
407 None,
408 None,
409 None,
410 None,
411 None,
412 None,
413 None,
414 0.into(),
415 0.into(),
416 );
417 assert!(result.is_err());
418 }
419
420 #[rstest]
421 fn test_new_checked_zero_price_increment() {
422 let result = Equity::new_checked(
423 InstrumentId::from("AAPL.XNAS"),
424 Symbol::from("AAPL"),
425 None,
426 Currency::USD(),
427 0,
428 Price::from("0"),
429 None,
430 None,
431 None,
432 None,
433 None,
434 None,
435 None,
436 None,
437 None,
438 None,
439 0.into(),
440 0.into(),
441 );
442 assert!(result.is_err());
443 }
444
445 #[rstest]
446 fn test_new_checked_non_ascii_isin() {
447 let result = Equity::new_checked(
448 InstrumentId::from("AAPL.XNAS"),
449 Symbol::from("AAPL"),
450 Some(ustr::Ustr::from("US\u{00E9}378331005")),
451 Currency::USD(),
452 2,
453 Price::from("0.01"),
454 None,
455 None,
456 None,
457 None,
458 None,
459 None,
460 None,
461 None,
462 None,
463 None,
464 0.into(),
465 0.into(),
466 );
467 assert!(result.is_err());
468 assert!(result.unwrap_err().to_string().contains("non-ASCII"));
469 }
470
471 #[rstest]
472 fn test_serialization_roundtrip(equity_aapl: Equity) {
473 let json = serde_json::to_string(&equity_aapl).unwrap();
474 let deserialized: Equity = serde_json::from_str(&json).unwrap();
475 assert_eq!(equity_aapl, deserialized);
476 }
477}