Skip to main content

quant_primitives/
crypto_classifier.rs

1//! Crypto classification heuristics — single source of truth.
2//!
3//! Used by both `Symbol::is_crypto()` and `Ticker::FromStr` to determine
4//! whether a trading pair is a cryptocurrency pair.
5
6/// Check if quote currency is a crypto quote (stablecoins, BTC, ETH, etc.).
7pub fn is_crypto_quote(quote: &str) -> bool {
8    matches!(
9        quote.to_uppercase().as_str(),
10        "USDT" | "USDC" | "BTC" | "ETH" | "BNB" | "BUSD" | "DAI" | "TUSD"
11    )
12}
13
14/// Check if base symbol is a known cryptocurrency.
15///
16/// This is the canonical source of truth for crypto base classification.
17/// Downstream consumers (qbot-core adapters, MCP tools) MUST call this
18/// function instead of maintaining local lists (#3446 split-brain fix).
19pub fn is_crypto_base(base: &str) -> bool {
20    matches!(
21        base.to_uppercase().as_str(),
22        // ── Layer 1: BTC + top-10 by market cap ─────────────────────────
23        "BTC"
24            | "ETH"
25            | "SOL"
26            | "ADA"
27            | "DOT"
28            | "AVAX"
29            | "MATIC"
30            | "LINK"
31            | "UNI"
32            | "ATOM"
33            | "XRP"
34            | "LTC"
35            | "BCH"
36            | "DOGE"
37            | "SHIB"
38            // ── Layer 2: established alts ────────────────────────────────
39            | "TRX"
40            | "ETC"
41            | "XLM"
42            | "XMR"
43            | "ALGO"
44            | "VET"
45            | "ICP"
46            | "FIL"
47            | "HBAR"
48            | "NEAR"
49            | "FTM"
50            | "APT"
51            // ── Layer 3: DeFi / governance ───────────────────────────────
52            | "AAVE"
53            | "MKR"
54            | "SUSHI"
55            | "COMP"
56            | "YFI"
57            | "SNX"
58            | "CRV"
59            | "BAL"
60            | "ZRX"
61            | "1INCH"
62            | "LDO"
63            // ── Layer 4: L2 / rollup tokens ──────────────────────────────
64            | "ARB"
65            | "OP"
66            // ── Layer 5: metaverse / gaming ──────────────────────────────
67            | "SAND"
68            | "MANA"
69            | "AXS"
70            // ── Layer 6: 2023-2024 wave ──────────────────────────────────
71            | "PEPE"
72            | "WIF"
73            | "BONK"
74            | "JUP"
75            | "RNDR"
76            | "INJ"
77            | "TIA"
78            | "SEI"
79            | "SUI"
80            | "BLUR"
81            | "PYTH"
82    )
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn original_bases_still_recognized() {
91        for sym in [
92            "BTC", "ETH", "SOL", "ADA", "DOT", "AVAX", "MATIC", "LINK", "UNI", "ATOM", "XRP",
93            "LTC", "BCH", "DOGE", "SHIB", "TRX", "ETC", "XLM", "XMR", "ALGO", "VET", "ICP", "FIL",
94            "AAVE", "MKR", "SUSHI", "COMP", "YFI", "SNX", "CRV", "BAL", "ZRX", "1INCH",
95        ] {
96            assert!(
97                is_crypto_base(sym),
98                "{sym} should be recognized as crypto base"
99            );
100        }
101    }
102
103    /// Regression #3446: these were in ephemeral KNOWN_CRYPTO but missing from
104    /// is_crypto_base(), causing classification divergence.
105    #[test]
106    fn newly_added_bases_recognized() {
107        for sym in [
108            "NEAR", "APT", "ARB", "OP", "FTM", "HBAR", "SAND", "MANA", "AXS", "LDO", "PEPE", "WIF",
109            "BONK", "JUP", "RNDR", "INJ", "TIA", "SEI", "SUI", "BLUR", "PYTH",
110        ] {
111            assert!(
112                is_crypto_base(sym),
113                "{sym} should be recognized as crypto base after #3446"
114            );
115        }
116    }
117
118    #[test]
119    fn case_insensitive() {
120        assert!(is_crypto_base("btc"));
121        assert!(is_crypto_base("near"));
122        assert!(is_crypto_base("Arb"));
123    }
124
125    #[test]
126    fn non_crypto_rejected() {
127        assert!(!is_crypto_base("AAPL"));
128        assert!(!is_crypto_base("USD"));
129        assert!(!is_crypto_base("XYZ"));
130    }
131
132    #[test]
133    fn crypto_quotes_recognized() {
134        for q in ["USDT", "USDC", "BTC", "ETH", "BNB", "BUSD", "DAI", "TUSD"] {
135            assert!(
136                is_crypto_quote(q),
137                "{q} should be recognized as crypto quote"
138            );
139        }
140    }
141
142    #[test]
143    fn fiat_not_crypto_quote() {
144        assert!(!is_crypto_quote("USD"));
145        assert!(!is_crypto_quote("EUR"));
146    }
147}