1use std::{
21 fmt::{Debug, Display, Formatter},
22 hash::{Hash, Hasher},
23 str::FromStr,
24};
25
26use nautilus_core::correctness::{FAILED, check_nonempty_string, check_valid_string_utf8};
27use serde::{Deserialize, Serialize, Serializer};
28use ustr::Ustr;
29
30#[allow(unused_imports, reason = "FIXED_PRECISION used in docs")]
31use super::fixed::{FIXED_PRECISION, check_fixed_precision};
32use crate::{currencies::CURRENCY_MAP, enums::CurrencyType};
33
34#[repr(C)]
38#[derive(Clone, Copy, Eq)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", frozen, eq, hash)
42)]
43pub struct Currency {
44 pub code: Ustr,
46 pub precision: u8,
48 pub iso4217: u16,
50 pub name: Ustr,
52 pub currency_type: CurrencyType,
54}
55
56impl Currency {
57 pub fn new_checked<T: AsRef<str>>(
70 code: T,
71 precision: u8,
72 iso4217: u16,
73 name: T,
74 currency_type: CurrencyType,
75 ) -> anyhow::Result<Self> {
76 let code = code.as_ref();
77 let name = name.as_ref();
78 check_valid_string_utf8(code, "code")?;
79 check_nonempty_string(name, "name")?;
80 check_fixed_precision(precision)?;
81 Ok(Self {
82 code: Ustr::from(code),
83 precision,
84 iso4217,
85 name: Ustr::from(name),
86 currency_type,
87 })
88 }
89
90 pub fn new<T: AsRef<str>>(
96 code: T,
97 precision: u8,
98 iso4217: u16,
99 name: T,
100 currency_type: CurrencyType,
101 ) -> Self {
102 Self::new_checked(code, precision, iso4217, name, currency_type).expect(FAILED)
103 }
104
105 pub fn register(currency: Self, overwrite: bool) -> anyhow::Result<()> {
114 let mut map = CURRENCY_MAP
115 .lock()
116 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
117
118 if !overwrite && map.contains_key(currency.code.as_str()) {
119 return Ok(());
121 }
122
123 map.insert(currency.code.to_string(), currency);
125 Ok(())
126 }
127
128 pub fn try_from_str(s: &str) -> Option<Self> {
130 let map_guard = CURRENCY_MAP.lock().ok()?;
131 map_guard.get(s).copied()
132 }
133
134 pub fn is_fiat(code: &str) -> anyhow::Result<bool> {
142 let currency = Self::from_str(code)?;
143 Ok(currency.currency_type == CurrencyType::Fiat)
144 }
145
146 pub fn is_crypto(code: &str) -> anyhow::Result<bool> {
154 let currency = Self::from_str(code)?;
155 Ok(currency.currency_type == CurrencyType::Crypto)
156 }
157
158 pub fn is_commodity_backed(code: &str) -> anyhow::Result<bool> {
167 let currency = Self::from_str(code)?;
168 Ok(currency.currency_type == CurrencyType::CommodityBacked)
169 }
170}
171
172impl PartialEq for Currency {
173 fn eq(&self, other: &Self) -> bool {
174 self.code == other.code
175 }
176}
177
178impl Hash for Currency {
179 fn hash<H: Hasher>(&self, state: &mut H) {
180 self.code.hash(state);
181 }
182}
183
184impl Debug for Currency {
185 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186 write!(
187 f,
188 "{}(code='{}', precision={}, iso4217={}, name='{}', currency_type={})",
189 stringify!(Currency),
190 self.code,
191 self.precision,
192 self.iso4217,
193 self.name,
194 self.currency_type,
195 )
196 }
197}
198
199impl Display for Currency {
200 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
201 write!(f, "{}", self.code)
202 }
203}
204
205impl FromStr for Currency {
206 type Err = anyhow::Error;
207
208 fn from_str(s: &str) -> anyhow::Result<Self> {
209 let map_guard = CURRENCY_MAP
210 .lock()
211 .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?;
212 map_guard
213 .get(s)
214 .copied()
215 .ok_or_else(|| anyhow::anyhow!("Unknown currency: {s}"))
216 }
217}
218
219impl<T: AsRef<str>> From<T> for Currency {
220 fn from(value: T) -> Self {
221 Self::from_str(value.as_ref()).expect(FAILED)
222 }
223}
224
225impl Serialize for Currency {
226 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
227 where
228 S: Serializer,
229 {
230 self.code.serialize(serializer)
231 }
232}
233
234impl<'de> Deserialize<'de> for Currency {
235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236 where
237 D: serde::Deserializer<'de>,
238 {
239 let currency_str: String = Deserialize::deserialize(deserializer)?;
240 Self::from_str(¤cy_str).map_err(serde::de::Error::custom)
241 }
242}
243
244#[cfg(test)]
248mod tests {
249 use rstest::rstest;
250
251 use crate::{enums::CurrencyType, types::Currency};
252
253 #[rstest]
254 fn test_debug() {
255 let currency = Currency::AUD();
256 assert_eq!(
257 format!("{currency:?}"),
258 format!(
259 "Currency(code='AUD', precision=2, iso4217=36, name='Australian dollar', currency_type=FIAT)"
260 )
261 );
262 }
263
264 #[rstest]
265 fn test_display() {
266 let currency = Currency::AUD();
267 assert_eq!(format!("{currency}"), "AUD");
268 }
269
270 #[rstest]
271 #[should_panic(expected = "code")]
272 fn test_invalid_currency_code() {
273 let _ = Currency::new("", 2, 840, "United States dollar", CurrencyType::Fiat);
274 }
275
276 #[cfg(not(feature = "defi"))]
277 #[rstest]
278 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
279 fn test_invalid_precision() {
280 let _ = Currency::new("USD", 19, 840, "United States dollar", CurrencyType::Fiat);
282 }
283
284 #[cfg(feature = "defi")]
285 #[rstest]
286 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `WEI_PRECISION`")]
287 fn test_invalid_precision() {
288 let _ = Currency::new("ETH", 19, 0, "Ethereum", CurrencyType::Crypto);
290 }
291
292 #[rstest]
293 fn test_register_no_overwrite() {
294 let currency1 = Currency::new("TEST1", 2, 999, "Test Currency 1", CurrencyType::Fiat);
295 Currency::register(currency1, false).unwrap();
296
297 let currency2 = Currency::new(
298 "TEST1",
299 2,
300 999,
301 "Test Currency 2 Updated",
302 CurrencyType::Fiat,
303 );
304 Currency::register(currency2, false).unwrap();
305
306 let found = Currency::try_from_str("TEST1").unwrap();
307 assert_eq!(found.name.as_str(), "Test Currency 1");
308 }
309
310 #[rstest]
311 fn test_register_with_overwrite() {
312 let currency1 = Currency::new("TEST2", 2, 998, "Test Currency 2", CurrencyType::Fiat);
313 Currency::register(currency1, false).unwrap();
314
315 let currency2 = Currency::new(
316 "TEST2",
317 2,
318 998,
319 "Test Currency 2 Overwritten",
320 CurrencyType::Fiat,
321 );
322 Currency::register(currency2, true).unwrap();
323
324 let found = Currency::try_from_str("TEST2").unwrap();
325 assert_eq!(found.name.as_str(), "Test Currency 2 Overwritten");
326 }
327
328 #[rstest]
329 fn test_new_for_fiat() {
330 let currency = Currency::new("AUD", 2, 36, "Australian dollar", CurrencyType::Fiat);
331 assert_eq!(currency, currency);
332 assert_eq!(currency.code.as_str(), "AUD");
333 assert_eq!(currency.precision, 2);
334 assert_eq!(currency.iso4217, 36);
335 assert_eq!(currency.name.as_str(), "Australian dollar");
336 assert_eq!(currency.currency_type, CurrencyType::Fiat);
337 }
338
339 #[rstest]
340 fn test_new_for_crypto() {
341 let currency = Currency::new("ETH", 8, 0, "Ether", CurrencyType::Crypto);
342 assert_eq!(currency, currency);
343 assert_eq!(currency.code.as_str(), "ETH");
344 assert_eq!(currency.precision, 8);
345 assert_eq!(currency.iso4217, 0);
346 assert_eq!(currency.name.as_str(), "Ether");
347 assert_eq!(currency.currency_type, CurrencyType::Crypto);
348 }
349
350 #[rstest]
351 fn test_try_from_str_valid() {
352 let test_currency = Currency::new("TEST", 2, 999, "Test Currency", CurrencyType::Fiat);
353 Currency::register(test_currency, true).unwrap();
354
355 let currency = Currency::try_from_str("TEST");
356 assert!(currency.is_some());
357 assert_eq!(currency.unwrap(), test_currency);
358 }
359
360 #[rstest]
361 fn test_try_from_str_invalid() {
362 let invalid_currency = Currency::try_from_str("INVALID");
363 assert!(invalid_currency.is_none());
364 }
365
366 #[rstest]
367 fn test_equality() {
368 let currency1 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat);
369 let currency2 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat);
370 assert_eq!(currency1, currency2);
371 }
372
373 #[rstest]
374 fn test_currency_partial_eq_only_checks_code() {
375 let c1 = Currency::new("ABC", 2, 999, "Currency ABC", CurrencyType::Fiat);
376 let c2 = Currency::new("ABC", 8, 100, "Completely Different", CurrencyType::Crypto);
377
378 assert_eq!(c1, c2, "Should be equal if 'code' is the same");
379 }
380
381 #[rstest]
382 fn test_is_fiat() {
383 let currency = Currency::new("TESTFIAT", 2, 840, "Test Fiat", CurrencyType::Fiat);
384 Currency::register(currency, true).unwrap();
385
386 let result = Currency::is_fiat("TESTFIAT");
387 assert!(result.is_ok());
388 assert!(
389 result.unwrap(),
390 "Expected TESTFIAT to be recognized as fiat"
391 );
392 }
393
394 #[rstest]
395 fn test_is_crypto() {
396 let currency = Currency::new("TESTCRYPTO", 8, 0, "Test Crypto", CurrencyType::Crypto);
397 Currency::register(currency, true).unwrap();
398
399 let result = Currency::is_crypto("TESTCRYPTO");
400 assert!(result.is_ok());
401 assert!(
402 result.unwrap(),
403 "Expected TESTCRYPTO to be recognized as crypto"
404 );
405 }
406
407 #[rstest]
408 fn test_is_commodity_backed() {
409 let currency = Currency::new("TESTGOLD", 5, 0, "Test Gold", CurrencyType::CommodityBacked);
410 Currency::register(currency, true).unwrap();
411
412 let result = Currency::is_commodity_backed("TESTGOLD");
413 assert!(result.is_ok());
414 assert!(
415 result.unwrap(),
416 "Expected TESTGOLD to be recognized as commodity-backed"
417 );
418 }
419
420 #[rstest]
421 fn test_is_fiat_unknown_currency() {
422 let result = Currency::is_fiat("NON_EXISTENT");
423 assert!(result.is_err(), "Should fail for unknown currency code");
424 }
425
426 #[rstest]
427 fn test_serialization_deserialization() {
428 let currency = Currency::USD();
429 let serialized = serde_json::to_string(¤cy).unwrap();
430 let deserialized: Currency = serde_json::from_str(&serialized).unwrap();
431 assert_eq!(currency, deserialized);
432 }
433}