Skip to main content

quant_primitives/
symbol.rs

1//! Trading symbol (e.g., BTC/USDT, AAPL)
2
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5use thiserror::Error;
6
7use crate::crypto_classifier::{is_crypto_base, is_crypto_quote};
8
9/// Errors from parsing a [`Symbol`] from a string.
10#[derive(Debug, Error, PartialEq, Eq)]
11pub enum SymbolError {
12    /// The provided string does not contain a valid separator (/ or -).
13    #[error("invalid symbol format: {0}")]
14    InvalidFormat(String),
15}
16
17/// A trading symbol representing a base/quote currency pair.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Symbol {
20    base: String,
21    quote: String,
22}
23
24impl Symbol {
25    /// Returns the base currency (e.g., "BTC" in BTC/USDT).
26    pub fn base(&self) -> &str {
27        &self.base
28    }
29
30    /// Returns the quote currency (e.g., "USDT" in BTC/USDT).
31    pub fn quote(&self) -> &str {
32        &self.quote
33    }
34
35    /// Returns the canonical `BASE/QUOTE` representation.
36    pub fn canonical(&self) -> String {
37        format!("{}/{}", self.base, self.quote)
38    }
39
40    /// Returns true if this is likely a crypto pair.
41    ///
42    /// Heuristic: checks both quote AND base currencies.
43    /// - Crypto quotes (USDT, USDC, BTC, ETH, etc.) → crypto
44    /// - Known crypto bases with fiat quote (BTC/USD, ETH/USD) → crypto
45    /// - Everything else → not crypto
46    ///
47    /// Aligned with `Ticker::FromStr` inference logic (#2693).
48    pub fn is_crypto(&self) -> bool {
49        is_crypto_quote(&self.quote) || is_crypto_base(&self.base)
50    }
51}
52
53impl FromStr for Symbol {
54    type Err = SymbolError;
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        // Slash separator (BTC/USDT)
58        if let Some((base, quote)) = s.split_once('/') {
59            return Ok(Self {
60                base: base.to_uppercase(),
61                quote: quote.to_uppercase(),
62            });
63        }
64        // Dash separator (ETH-USD)
65        if let Some((base, quote)) = s.split_once('-') {
66            return Ok(Self {
67                base: base.to_uppercase(),
68                quote: quote.to_uppercase(),
69            });
70        }
71        Err(SymbolError::InvalidFormat(s.to_string()))
72    }
73}
74
75#[cfg(test)]
76#[path = "symbol_tests.rs"]
77mod tests;