1use std::{
21 fmt::{Debug, Display},
22 hash::{Hash, Hasher},
23 str::FromStr,
24};
25
26use nautilus_core::correctness::{
27 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED, check_nonempty_string,
28 check_valid_string_utf8,
29};
30use serde::{Deserialize, Serialize, Serializer};
31use ustr::Ustr;
32
33#[allow(unused_imports)]
34use super::fixed::{FIXED_PRECISION, check_fixed_precision};
35use crate::{currencies::CURRENCY_MAP, enums::CurrencyType};
36
37#[repr(C)]
41#[derive(Clone, Copy, Eq)]
42#[cfg_attr(
43 feature = "python",
44 pyo3::pyclass(
45 module = "nautilus_trader.core.nautilus_pyo3.model",
46 frozen,
47 eq,
48 hash,
49 from_py_object
50 )
51)]
52#[cfg_attr(
53 feature = "python",
54 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
55)]
56pub struct Currency {
57 pub code: Ustr,
59 pub precision: u8,
61 pub iso4217: u16,
63 pub name: Ustr,
65 pub currency_type: CurrencyType,
67}
68
69impl Currency {
70 pub fn new_checked<T: AsRef<str>>(
83 code: T,
84 precision: u8,
85 iso4217: u16,
86 name: T,
87 currency_type: CurrencyType,
88 ) -> CorrectnessResult<Self> {
89 let code = code.as_ref();
90 let name = name.as_ref();
91 check_valid_string_utf8(code, "code")?;
92 check_nonempty_string(name, "name")?;
93 check_fixed_precision(precision)?;
94 Ok(Self {
95 code: Ustr::from(code),
96 precision,
97 iso4217,
98 name: Ustr::from(name),
99 currency_type,
100 })
101 }
102
103 pub fn new<T: AsRef<str>>(
109 code: T,
110 precision: u8,
111 iso4217: u16,
112 name: T,
113 currency_type: CurrencyType,
114 ) -> Self {
115 Self::new_checked(code, precision, iso4217, name, currency_type).expect_display(FAILED)
116 }
117
118 pub fn register(currency: Self, overwrite: bool) -> CorrectnessResult<()> {
127 let mut map = CURRENCY_MAP
128 .lock()
129 .map_err(|e| CorrectnessError::PredicateViolation {
130 message: format!("Failed to acquire lock on `CURRENCY_MAP`: {e}"),
131 })?;
132
133 if !overwrite && map.contains_key(currency.code.as_str()) {
134 return Ok(());
136 }
137
138 map.insert(currency.code.to_string(), currency);
140 Ok(())
141 }
142
143 pub fn try_from_str(s: &str) -> Option<Self> {
145 let map_guard = CURRENCY_MAP.lock().ok()?;
146 map_guard.get(s).copied()
147 }
148
149 pub fn is_fiat(code: &str) -> anyhow::Result<bool> {
157 let currency = Self::from_str(code)?;
158 Ok(currency.currency_type == CurrencyType::Fiat)
159 }
160
161 pub fn is_crypto(code: &str) -> anyhow::Result<bool> {
169 let currency = Self::from_str(code)?;
170 Ok(currency.currency_type == CurrencyType::Crypto)
171 }
172
173 pub fn is_commodity_backed(code: &str) -> anyhow::Result<bool> {
182 let currency = Self::from_str(code)?;
183 Ok(currency.currency_type == CurrencyType::CommodityBacked)
184 }
185
186 #[must_use]
197 pub fn get_or_create_crypto<T: AsRef<str>>(code: T) -> Self {
198 let code_str = code.as_ref();
199 Self::try_from_str(code_str).unwrap_or_else(|| {
200 let currency = Self::new(code_str, 8, 0, code_str, CurrencyType::Crypto);
201
202 if let Err(e) = Self::register(currency, false) {
203 log::error!("Failed to register currency '{code_str}': {e}");
204 }
205
206 currency
207 })
208 }
209
210 #[must_use]
224 pub fn get_or_create_crypto_with_context<T: AsRef<str>>(
225 code: T,
226 context: Option<&str>,
227 ) -> Self {
228 let trimmed = code.as_ref().trim();
229 let ctx = context.unwrap_or("unknown");
230
231 if trimmed.is_empty() {
232 log::warn!(
233 "get_or_create_crypto_with_context called with empty code (context: {ctx}), using USDT as fallback"
234 );
235 return Self::USDT();
236 }
237
238 Self::get_or_create_crypto(trimmed)
239 }
240}
241
242impl PartialEq for Currency {
243 fn eq(&self, other: &Self) -> bool {
244 self.code == other.code
245 }
246}
247
248impl Hash for Currency {
249 fn hash<H: Hasher>(&self, state: &mut H) {
250 self.code.hash(state);
251 }
252}
253
254impl Debug for Currency {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(
257 f,
258 "{}(code='{}', precision={}, iso4217={}, name='{}', currency_type={})",
259 stringify!(Currency),
260 self.code,
261 self.precision,
262 self.iso4217,
263 self.name,
264 self.currency_type,
265 )
266 }
267}
268
269impl Display for Currency {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 write!(f, "{}", self.code)
272 }
273}
274
275impl FromStr for Currency {
276 type Err = anyhow::Error;
277
278 fn from_str(s: &str) -> anyhow::Result<Self> {
279 let map_guard = CURRENCY_MAP
280 .lock()
281 .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?;
282 map_guard
283 .get(s)
284 .copied()
285 .ok_or_else(|| anyhow::anyhow!("Unknown currency: {s}"))
286 }
287}
288
289impl<T: AsRef<str>> From<T> for Currency {
290 fn from(value: T) -> Self {
291 Self::from_str(value.as_ref()).expect(FAILED)
292 }
293}
294
295impl Serialize for Currency {
296 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
297 where
298 S: Serializer,
299 {
300 self.code.serialize(serializer)
301 }
302}
303
304impl<'de> Deserialize<'de> for Currency {
305 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
306 where
307 D: serde::Deserializer<'de>,
308 {
309 let currency_str: String = Deserialize::deserialize(deserializer)?;
310 Self::from_str(¤cy_str).map_err(serde::de::Error::custom)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use rstest::rstest;
317
318 use crate::{enums::CurrencyType, types::Currency};
319
320 #[rstest]
321 fn test_debug() {
322 let currency = Currency::AUD();
323 assert_eq!(
324 format!("{currency:?}"),
325 format!(
326 "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)"
327 )
328 );
329 }
330
331 #[rstest]
332 fn test_display() {
333 let currency = Currency::AUD();
334 assert_eq!(format!("{currency}"), "AUD");
335 }
336
337 #[rstest]
338 #[should_panic(expected = "code")]
339 fn test_invalid_currency_code() {
340 let _ = Currency::new("", 2, 840, "United States dollar", CurrencyType::Fiat);
341 }
342
343 #[cfg(not(feature = "defi"))]
344 #[rstest]
345 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
346 fn test_invalid_precision() {
347 let _ = Currency::new("USD", 19, 840, "United States dollar", CurrencyType::Fiat);
349 }
350
351 #[cfg(feature = "defi")]
352 #[rstest]
353 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `WEI_PRECISION`")]
354 fn test_invalid_precision() {
355 let _ = Currency::new("ETH", 19, 0, "Ethereum", CurrencyType::Crypto);
357 }
358
359 #[rstest]
360 fn test_register_no_overwrite() {
361 let currency1 = Currency::new("TEST1", 2, 999, "Test Currency 1", CurrencyType::Fiat);
362 Currency::register(currency1, false).unwrap();
363
364 let currency2 = Currency::new(
365 "TEST1",
366 2,
367 999,
368 "Test Currency 2 Updated",
369 CurrencyType::Fiat,
370 );
371 Currency::register(currency2, false).unwrap();
372
373 let found = Currency::try_from_str("TEST1").unwrap();
374 assert_eq!(found.name.as_str(), "Test Currency 1");
375 }
376
377 #[rstest]
378 fn test_register_with_overwrite() {
379 let currency1 = Currency::new("TEST2", 2, 998, "Test Currency 2", CurrencyType::Fiat);
380 Currency::register(currency1, false).unwrap();
381
382 let currency2 = Currency::new(
383 "TEST2",
384 2,
385 998,
386 "Test Currency 2 Overwritten",
387 CurrencyType::Fiat,
388 );
389 Currency::register(currency2, true).unwrap();
390
391 let found = Currency::try_from_str("TEST2").unwrap();
392 assert_eq!(found.name.as_str(), "Test Currency 2 Overwritten");
393 }
394
395 #[rstest]
396 fn test_new_for_fiat() {
397 let currency = Currency::new("AUD", 2, 36, "Australian dollar", CurrencyType::Fiat);
398 assert_eq!(currency, currency);
399 assert_eq!(currency.code.as_str(), "AUD");
400 assert_eq!(currency.precision, 2);
401 assert_eq!(currency.iso4217, 36);
402 assert_eq!(currency.name.as_str(), "Australian dollar");
403 assert_eq!(currency.currency_type, CurrencyType::Fiat);
404 }
405
406 #[rstest]
407 fn test_new_for_crypto() {
408 let currency = Currency::new("ETH", 8, 0, "Ether", CurrencyType::Crypto);
409 assert_eq!(currency, currency);
410 assert_eq!(currency.code.as_str(), "ETH");
411 assert_eq!(currency.precision, 8);
412 assert_eq!(currency.iso4217, 0);
413 assert_eq!(currency.name.as_str(), "Ether");
414 assert_eq!(currency.currency_type, CurrencyType::Crypto);
415 }
416
417 #[rstest]
418 fn test_try_from_str_valid() {
419 let test_currency = Currency::new("TEST", 2, 999, "Test Currency", CurrencyType::Fiat);
420 Currency::register(test_currency, true).unwrap();
421
422 let currency = Currency::try_from_str("TEST");
423 assert!(currency.is_some());
424 assert_eq!(currency.unwrap(), test_currency);
425 }
426
427 #[rstest]
428 fn test_try_from_str_invalid() {
429 let invalid_currency = Currency::try_from_str("INVALID");
430 assert!(invalid_currency.is_none());
431 }
432
433 #[rstest]
434 fn test_equality() {
435 let currency1 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat);
436 let currency2 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat);
437 assert_eq!(currency1, currency2);
438 }
439
440 #[rstest]
441 fn test_currency_partial_eq_only_checks_code() {
442 let c1 = Currency::new("ABC", 2, 999, "Currency ABC", CurrencyType::Fiat);
443 let c2 = Currency::new("ABC", 8, 100, "Completely Different", CurrencyType::Crypto);
444
445 assert_eq!(c1, c2, "Should be equal if 'code' is the same");
446 }
447
448 #[rstest]
449 fn test_is_fiat() {
450 let currency = Currency::new("TESTFIAT", 2, 840, "Test Fiat", CurrencyType::Fiat);
451 Currency::register(currency, true).unwrap();
452
453 let result = Currency::is_fiat("TESTFIAT");
454 assert!(result.is_ok());
455 assert!(
456 result.unwrap(),
457 "Expected TESTFIAT to be recognized as fiat"
458 );
459 }
460
461 #[rstest]
462 fn test_is_crypto() {
463 let currency = Currency::new("TESTCRYPTO", 8, 0, "Test Crypto", CurrencyType::Crypto);
464 Currency::register(currency, true).unwrap();
465
466 let result = Currency::is_crypto("TESTCRYPTO");
467 assert!(result.is_ok());
468 assert!(
469 result.unwrap(),
470 "Expected TESTCRYPTO to be recognized as crypto"
471 );
472 }
473
474 #[rstest]
475 fn test_is_commodity_backed() {
476 let currency = Currency::new("TESTGOLD", 5, 0, "Test Gold", CurrencyType::CommodityBacked);
477 Currency::register(currency, true).unwrap();
478
479 let result = Currency::is_commodity_backed("TESTGOLD");
480 assert!(result.is_ok());
481 assert!(
482 result.unwrap(),
483 "Expected TESTGOLD to be recognized as commodity-backed"
484 );
485 }
486
487 #[rstest]
488 fn test_is_fiat_unknown_currency() {
489 let result = Currency::is_fiat("NON_EXISTENT");
490 assert!(result.is_err(), "Should fail for unknown currency code");
491 }
492
493 #[rstest]
494 fn test_serialization_deserialization() {
495 let currency = Currency::USD();
496 let serialized = serde_json::to_string(¤cy).unwrap();
497 let deserialized: Currency = serde_json::from_str(&serialized).unwrap();
498 assert_eq!(currency, deserialized);
499 }
500
501 #[rstest]
502 fn test_get_or_create_crypto_existing() {
503 let currency = Currency::get_or_create_crypto("BTC");
505 assert_eq!(currency.code.as_str(), "BTC");
506 assert_eq!(currency.currency_type, CurrencyType::Crypto);
507 }
508
509 #[rstest]
510 fn test_get_or_create_crypto_new() {
511 let currency = Currency::get_or_create_crypto("NEWCOIN");
513 assert_eq!(currency.code.as_str(), "NEWCOIN");
514 assert_eq!(currency.precision, 8);
515 assert_eq!(currency.iso4217, 0);
516 assert_eq!(currency.name.as_str(), "NEWCOIN");
517 assert_eq!(currency.currency_type, CurrencyType::Crypto);
518
519 let retrieved = Currency::try_from_str("NEWCOIN");
521 assert!(retrieved.is_some());
522 assert_eq!(retrieved.unwrap(), currency);
523 }
524
525 #[rstest]
526 fn test_get_or_create_crypto_idempotent() {
527 let currency1 = Currency::get_or_create_crypto("TESTCOIN");
529
530 let currency2 = Currency::get_or_create_crypto("TESTCOIN");
532
533 assert_eq!(currency1, currency2);
534 }
535
536 #[rstest]
537 fn test_get_or_create_crypto_with_ustr() {
538 use ustr::Ustr;
539
540 let code = Ustr::from("USTRCOIN");
542 let currency = Currency::get_or_create_crypto(code);
543 assert_eq!(currency.code.as_str(), "USTRCOIN");
544 assert_eq!(currency.currency_type, CurrencyType::Crypto);
545 }
546
547 #[rstest]
548 fn test_get_or_create_crypto_with_context_valid() {
549 let result = Currency::get_or_create_crypto_with_context("BTC", Some("test context"));
550 assert_eq!(result, Currency::BTC());
551 }
552
553 #[rstest]
554 fn test_get_or_create_crypto_with_context_empty() {
555 let result = Currency::get_or_create_crypto_with_context("", Some("test context"));
556 assert_eq!(result, Currency::USDT());
557 }
558
559 #[rstest]
560 fn test_get_or_create_crypto_with_context_whitespace() {
561 let result = Currency::get_or_create_crypto_with_context(" ", Some("test context"));
562 assert_eq!(result, Currency::USDT());
563 }
564
565 #[rstest]
566 fn test_get_or_create_crypto_with_context_unknown() {
567 let result = Currency::get_or_create_crypto_with_context("NEWCOIN", Some("test context"));
569 assert_eq!(result.code.as_str(), "NEWCOIN");
570 assert_eq!(result.precision, 8);
571 }
572}