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