quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
//! Trading symbol (e.g., BTC/USDT, AAPL)

use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;

use crate::crypto_classifier::{is_crypto_base, is_crypto_quote};

/// Errors from parsing a [`Symbol`] from a string.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SymbolError {
    /// The provided string does not contain a valid separator (/ or -).
    #[error("invalid symbol format: {0}")]
    InvalidFormat(String),
}

/// A trading symbol representing a base/quote currency pair.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Symbol {
    base: String,
    quote: String,
}

impl Symbol {
    /// Returns the base currency (e.g., "BTC" in BTC/USDT).
    pub fn base(&self) -> &str {
        &self.base
    }

    /// Returns the quote currency (e.g., "USDT" in BTC/USDT).
    pub fn quote(&self) -> &str {
        &self.quote
    }

    /// Returns the canonical `BASE/QUOTE` representation.
    pub fn canonical(&self) -> String {
        format!("{}/{}", self.base, self.quote)
    }

    /// Returns true if this is likely a crypto pair.
    ///
    /// Heuristic: checks both quote AND base currencies.
    /// - Crypto quotes (USDT, USDC, BTC, ETH, etc.) → crypto
    /// - Known crypto bases with fiat quote (BTC/USD, ETH/USD) → crypto
    /// - Everything else → not crypto
    ///
    /// Aligned with `Ticker::FromStr` inference logic (#2693).
    pub fn is_crypto(&self) -> bool {
        is_crypto_quote(&self.quote) || is_crypto_base(&self.base)
    }
}

impl FromStr for Symbol {
    type Err = SymbolError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Slash separator (BTC/USDT)
        if let Some((base, quote)) = s.split_once('/') {
            return Ok(Self {
                base: base.to_uppercase(),
                quote: quote.to_uppercase(),
            });
        }
        // Dash separator (ETH-USD)
        if let Some((base, quote)) = s.split_once('-') {
            return Ok(Self {
                base: base.to_uppercase(),
                quote: quote.to_uppercase(),
            });
        }
        Err(SymbolError::InvalidFormat(s.to_string()))
    }
}

#[cfg(test)]
#[path = "symbol_tests.rs"]
mod tests;