quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
//! Crypto classification heuristics — single source of truth.
//!
//! Used by both `Symbol::is_crypto()` and `Ticker::FromStr` to determine
//! whether a trading pair is a cryptocurrency pair.

/// Check if quote currency is a crypto quote (stablecoins, BTC, ETH, etc.).
pub fn is_crypto_quote(quote: &str) -> bool {
    matches!(
        quote.to_uppercase().as_str(),
        "USDT" | "USDC" | "BTC" | "ETH" | "BNB" | "BUSD" | "DAI" | "TUSD"
    )
}

/// Check if base symbol is a known cryptocurrency.
///
/// This is the canonical source of truth for crypto base classification.
/// Downstream consumers (qbot-core adapters, MCP tools) MUST call this
/// function instead of maintaining local lists (#3446 split-brain fix).
pub fn is_crypto_base(base: &str) -> bool {
    matches!(
        base.to_uppercase().as_str(),
        // ── Layer 1: BTC + top-10 by market cap ─────────────────────────
        "BTC"
            | "ETH"
            | "SOL"
            | "ADA"
            | "DOT"
            | "AVAX"
            | "MATIC"
            | "LINK"
            | "UNI"
            | "ATOM"
            | "XRP"
            | "LTC"
            | "BCH"
            | "DOGE"
            | "SHIB"
            // ── Layer 2: established alts ────────────────────────────────
            | "TRX"
            | "ETC"
            | "XLM"
            | "XMR"
            | "ALGO"
            | "VET"
            | "ICP"
            | "FIL"
            | "HBAR"
            | "NEAR"
            | "FTM"
            | "APT"
            // ── Layer 3: DeFi / governance ───────────────────────────────
            | "AAVE"
            | "MKR"
            | "SUSHI"
            | "COMP"
            | "YFI"
            | "SNX"
            | "CRV"
            | "BAL"
            | "ZRX"
            | "1INCH"
            | "LDO"
            // ── Layer 4: L2 / rollup tokens ──────────────────────────────
            | "ARB"
            | "OP"
            // ── Layer 5: metaverse / gaming ──────────────────────────────
            | "SAND"
            | "MANA"
            | "AXS"
            // ── Layer 6: 2023-2024 wave ──────────────────────────────────
            | "PEPE"
            | "WIF"
            | "BONK"
            | "JUP"
            | "RNDR"
            | "INJ"
            | "TIA"
            | "SEI"
            | "SUI"
            | "BLUR"
            | "PYTH"
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn original_bases_still_recognized() {
        for sym in [
            "BTC", "ETH", "SOL", "ADA", "DOT", "AVAX", "MATIC", "LINK", "UNI", "ATOM", "XRP",
            "LTC", "BCH", "DOGE", "SHIB", "TRX", "ETC", "XLM", "XMR", "ALGO", "VET", "ICP", "FIL",
            "AAVE", "MKR", "SUSHI", "COMP", "YFI", "SNX", "CRV", "BAL", "ZRX", "1INCH",
        ] {
            assert!(
                is_crypto_base(sym),
                "{sym} should be recognized as crypto base"
            );
        }
    }

    /// Regression #3446: these were in ephemeral KNOWN_CRYPTO but missing from
    /// is_crypto_base(), causing classification divergence.
    #[test]
    fn newly_added_bases_recognized() {
        for sym in [
            "NEAR", "APT", "ARB", "OP", "FTM", "HBAR", "SAND", "MANA", "AXS", "LDO", "PEPE", "WIF",
            "BONK", "JUP", "RNDR", "INJ", "TIA", "SEI", "SUI", "BLUR", "PYTH",
        ] {
            assert!(
                is_crypto_base(sym),
                "{sym} should be recognized as crypto base after #3446"
            );
        }
    }

    #[test]
    fn case_insensitive() {
        assert!(is_crypto_base("btc"));
        assert!(is_crypto_base("near"));
        assert!(is_crypto_base("Arb"));
    }

    #[test]
    fn non_crypto_rejected() {
        assert!(!is_crypto_base("AAPL"));
        assert!(!is_crypto_base("USD"));
        assert!(!is_crypto_base("XYZ"));
    }

    #[test]
    fn crypto_quotes_recognized() {
        for q in ["USDT", "USDC", "BTC", "ETH", "BNB", "BUSD", "DAI", "TUSD"] {
            assert!(
                is_crypto_quote(q),
                "{q} should be recognized as crypto quote"
            );
        }
    }

    #[test]
    fn fiat_not_crypto_quote() {
        assert!(!is_crypto_quote("USD"));
        assert!(!is_crypto_quote("EUR"));
    }
}