finance-query 2.8.0

A Rust library for querying financial data
Documentation
//! Compile and runtime tests for docs/library/crypto.md
//!
//! Requires the `crypto` feature flag:
//!   cargo test --test doc_crypto --features crypto
//!   cargo test --test doc_crypto --features crypto -- --ignored   (network tests)

#![cfg(feature = "crypto")]

use finance_query::crypto::CoinQuote;

// ---------------------------------------------------------------------------
// CoinQuote — compile-time field verification
// ---------------------------------------------------------------------------

/// Verifies all CoinQuote fields documented in crypto.md exist with correct
/// types. Never called; exists only for the compiler to type-check.
#[allow(dead_code)]
fn _verify_coin_quote_fields(c: CoinQuote) {
    let _: String = c.id;
    let _: String = c.symbol;
    let _: String = c.name;
    let _: Option<f64> = c.current_price;
    let _: Option<f64> = c.market_cap;
    let _: Option<u32> = c.market_cap_rank;
    let _: Option<f64> = c.price_change_percentage_24h;
    let _: Option<f64> = c.total_volume;
    let _: Option<f64> = c.circulating_supply;
    let _: Option<String> = c.image;
}

// ---------------------------------------------------------------------------
// Network tests
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_crypto_top_coins() {
    use finance_query::crypto;

    let top = crypto::coins("usd", 10).await.unwrap();
    assert!(!top.is_empty(), "should return coins");
    assert!(top.len() <= 10);

    for coin in &top {
        let price = coin.current_price.unwrap_or(0.0);
        let change = coin.price_change_percentage_24h.unwrap_or(0.0);
        let rank = coin.market_cap_rank.unwrap_or(0);
        println!(
            "#{} {} ({}): ${:.2} ({:+.2}%)",
            rank, coin.name, coin.symbol, price, change
        );
    }
}

#[tokio::test]
#[ignore = "requires network access"]
async fn test_crypto_single_coin_bitcoin() {
    use finance_query::crypto;

    let btc = crypto::coin("bitcoin", "usd").await.unwrap();
    assert_eq!(btc.id, "bitcoin");
    assert_eq!(btc.symbol.to_uppercase(), "BTC");
    let price = btc.current_price.unwrap_or(0.0);
    assert!(price > 0.0, "BTC price should be positive");
    println!("Bitcoin: ${:.2}", price);
}

#[tokio::test]
#[ignore = "requires network access"]
async fn test_crypto_single_coin_ethereum() {
    use finance_query::crypto;

    let eth = crypto::coin("ethereum", "usd").await.unwrap();
    let mktcap = eth.market_cap.unwrap_or(0.0);
    println!("Ethereum market cap: ${:.2}B", mktcap / 1e9);
    assert!(mktcap > 0.0);
}

// ---------------------------------------------------------------------------
// CoinQuote fields — compile-time verification matching crypto.md table
// ---------------------------------------------------------------------------

/// Mirrors the `verify_coin_quote_fields` block in crypto.md.
#[allow(dead_code)]
fn _verify_coin_quote_fields_doc(c: CoinQuote) {
    let _: String = c.id;
    let _: String = c.symbol;
    let _: String = c.name;
    let _: Option<f64> = c.current_price;
    let _: Option<f64> = c.market_cap;
    let _: Option<u32> = c.market_cap_rank;
    let _: Option<f64> = c.price_change_percentage_24h;
    let _: Option<f64> = c.total_volume;
    let _: Option<f64> = c.circulating_supply;
    let _: Option<String> = c.image;
}

/// Mirrors the `verify_crypto_quote_price` block in crypto.md.
#[allow(dead_code)]
fn _verify_crypto_quote_price(q: finance_query::CryptoQuote) {
    let _: Option<f64> = q.price;
}

// ---------------------------------------------------------------------------
// Compile-time: Indicators & Risk section of crypto.md
// ---------------------------------------------------------------------------

/// Verifies the documented `CryptoCoin::indicators`/`indicator`/`risk` flow
/// type-checks. Never called; exists only for the compiler to type-check.
#[allow(dead_code)]
#[cfg(all(feature = "indicators", feature = "risk"))]
async fn _verify_crypto_coin_indicators_and_risk() -> finance_query::Result<()> {
    use finance_query::indicators::Indicator;
    use finance_query::{Capability, Interval, Provider, Providers, TimeRange};

    let providers = Providers::builder()
        .route(Capability::CRYPTO, [Provider::CoinGecko])
        .build()
        .await?;
    let btc = providers.crypto("BTC");

    let summary = btc
        .indicators("usd", Interval::OneDay, TimeRange::ThreeMonths)
        .await?;
    let _: Option<f64> = summary.rsi_14;

    let _rsi_21 = btc
        .indicator(
            Indicator::Rsi(21),
            "usd",
            Interval::OneDay,
            TimeRange::ThreeMonths,
        )
        .await?;

    let risk = btc
        .risk("usd", Interval::OneDay, TimeRange::OneYear)
        .await?;
    let _: f64 = risk.var_95;
    let _: f64 = risk.max_drawdown;

    Ok(())
}

// ---------------------------------------------------------------------------
// CryptoCoin handle — network test
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires network access"]
async fn test_crypto_coin_handle() {
    use finance_query::{Capability, Interval, Provider, Providers, TimeRange};
    let providers = Providers::builder()
        .route(Capability::CRYPTO, [Provider::CoinGecko])
        .build()
        .await
        .unwrap();

    // `quote` is keyed by the CoinGecko coin *id* (keyless via CoinGecko).
    let btc = providers.crypto("bitcoin");
    let quote = btc.quote("usd").await.unwrap();
    assert!(quote.price.unwrap_or(0.0) > 0.0);

    // `chart`/`history` route through `Capability::CHART`; on the default Yahoo
    // route the handle id must be the coin's *ticker* (`"BTC"` -> `"BTC-USD"`).
    let btc_chart = providers.crypto("BTC");
    let chart = btc_chart
        .chart("usd", Interval::OneDay, TimeRange::OneMonth)
        .await
        .unwrap();
    assert!(!chart.candles.is_empty());
    let history = btc_chart.history("usd", TimeRange::OneMonth).await.unwrap();
    assert!(!history.candles.is_empty());
}