quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
use super::*;

#[test]
fn parse_slash_format() {
    let sym: Symbol = "BTC/USDT"
        .parse()
        .expect("'BTC/USDT' is a valid Symbol literal");
    assert_eq!(sym.base(), "BTC");
    assert_eq!(sym.quote(), "USDT");
}

#[test]
fn parse_dash_format() {
    let sym: Symbol = "ETH-USD"
        .parse()
        .expect("'ETH-USD' is a valid Symbol literal");
    assert_eq!(sym.base(), "ETH");
    assert_eq!(sym.quote(), "USD");
}

#[test]
fn canonical_format() {
    let sym: Symbol = "eth-btc"
        .parse()
        .expect("'eth-btc' is a valid Symbol literal");
    assert_eq!(sym.canonical(), "ETH/BTC");
}

#[test]
fn parse_invalid_format_returns_error() {
    let result: Result<Symbol, _> = "BTCUSDT".parse();
    assert!(matches!(result, Err(SymbolError::InvalidFormat(s)) if s == "BTCUSDT"));
}

#[test]
fn parse_empty_returns_error() {
    let result: Result<Symbol, _> = "".parse();
    assert!(matches!(result, Err(SymbolError::InvalidFormat(_))));
}

#[test]
fn is_crypto_true_for_stablecoins() {
    assert!("BTC/USDT"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
    assert!("ETH/USDC"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
    assert!("SOL/BUSD"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
    assert!("DOGE/DAI"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
}

#[test]
fn is_crypto_true_for_btc_eth_pairs() {
    assert!("ETH/BTC"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
    assert!("SOL/ETH"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
}

#[test]
fn is_crypto_false_for_fiat() {
    assert!(!"AAPL/USD"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
    assert!(!"EUR/GBP"
        .parse::<Symbol>()
        .expect("valid Symbol")
        .is_crypto());
}

/// Regression test for #2693: Symbol::is_crypto() must check BOTH quote and base.
///
/// BTC/USD, ETH/USD, SOL/USD are crypto assets traded against fiat.
/// The quote-only heuristic misclassifies them as non-crypto because "USD"
/// is not in the crypto quote list. Must also check the base currency.
#[test]
fn is_crypto_true_for_crypto_base_with_fiat_quote() {
    // These are crypto assets quoted in fiat — is_crypto() must return true
    assert!(
        "BTC/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "BTC/USD should be crypto (BTC is a cryptocurrency)"
    );
    assert!(
        "ETH/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "ETH/USD should be crypto (ETH is a cryptocurrency)"
    );
    assert!(
        "SOL/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "SOL/USD should be crypto (SOL is a cryptocurrency)"
    );
}

/// #2693: Equities must still NOT be classified as crypto
#[test]
fn is_crypto_false_for_equities_with_fiat_quote() {
    assert!(
        !"TSLA/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "TSLA/USD should NOT be crypto (TSLA is an equity)"
    );
    assert!(
        !"AAPL/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "AAPL/USD should NOT be crypto (AAPL is an equity)"
    );
    assert!(
        !"MSFT/USD"
            .parse::<Symbol>()
            .expect("valid Symbol")
            .is_crypto(),
        "MSFT/USD should NOT be crypto (MSFT is an equity)"
    );
}

// === Serde round-trip ===

#[test]
fn serde_roundtrip() {
    let original: Symbol = "SOL/USDC".parse().expect("valid symbol");
    let json = serde_json::to_string(&original).expect("valid json");
    let parsed: Symbol = serde_json::from_str(&json).expect("valid json");
    assert_eq!(parsed, original);
    assert_eq!(parsed.base(), "SOL");
    assert_eq!(parsed.quote(), "USDC");
}

// === SymbolError tests ===

#[test]
fn symbol_error_display_contains_input() {
    let err = SymbolError::InvalidFormat("BTCUSDT".to_string());
    assert!(err.to_string().contains("BTCUSDT"));
}

// === Normalization ===

#[test]
fn parse_normalizes_to_uppercase() {
    let sym: Symbol = "btc/usdt".parse().expect("valid symbol");
    assert_eq!(sym.base(), "BTC");
    assert_eq!(sym.quote(), "USDT");
}