finance-query v3.0.0

Multi-Provider Architecture#

abstract · Cargo Docs

docs.rs/finance-query — providers

Finance Query v3 introduces a provider abstraction layer that lets you route each data capability (quotes, charts, fundamentals, etc.) to a different provider through a single builder API. The system automatically falls back to the next provider in the list on failure.

Why Multiple Providers?#

Available Providers#

Yahoo Finance is always available with no configuration. All others are opt-in via feature flags:

Provider·verified current
ProviderFeature flagFree tierEnv var
Yahoo Finance(always available)Keyless
Polygon.iopolygon5 req/secPOLYGON_API_KEY
FMPfmp250 req/dayFMP_API_KEY
Alpha Vantagealphavantage25 req/dayALPHAVANTAGE_API_KEY
CoinGeckocrypto30 req/min(keyless)
FREDfred120 req/minFRED_API_KEY
World BankworldbankKeyless(keyless)
US Treasury FiscalDatafiscaldataKeyless(keyless)
BLSblsKeyless 25/day, keyed 500/dayBLS_API_KEY (optional)
FrankfurterfrankfurterKeyless(keyless)
BinancebinanceKeyless(keyless)
KrakenkrakenKeyless(keyless)
FINRAfinraKeyless (non-commercial)(keyless)
OpenFIGI (not provider-routed)openfigiKeyless 25 req/minOPENFIGI_API_KEY (optional)
DefiLlamadefiKeyless(keyless)
GDELT DOC 2.0gdeltKeyless (~1 req/5s)(keyless)
CFTCcftcKeyless(keyless)
Congressional Trades (House/Senate)housetrades and/or senatetradesKeyless(keyless)
NasdaqnasdaqKeyless (~2 req/s)(keyless)
WikipediawikipediaKeyless (~1 req/s)(keyless)
SEC EDGAR(always available)Keyless(email via edgar::init)
Local Market Calendar(always available)Keyless, no network call(keyless)
Local Exchange(always available)Keyless, no network call(keyless)
Custom(your own adapter)n/a(yours)
toml
[dependencies]
finance-query = { version = "3", features = ["polygon", "fmp"] }

Provider Initialization#

API keys are read from environment variables automatically during build(). No manual init calls are needed:

bash
export POLYGON_API_KEY="your-polygon-key"
export FMP_API_KEY="your-fmp-key"
export ALPHAVANTAGE_API_KEY="your-av-key"
export FRED_API_KEY="your-fred-key"

info · EDGAR requires a one-time init

The SEC EDGAR module requires edgar::init("user@example.com")? once per process (SEC policy requires contact info for rate limiting). See EDGAR.

Capability Routing#

Use .route(Capability, &[Provider]) on Providers::builder() to assign providers to specific data capabilities, then create handles via providers.ticker(). Providers are tried in order — the first success wins.

rust · no_run feature=full
use finance_query::{Capability, Fetch, Provider, Providers};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let providers = Providers::builder()
        // Route quotes to Polygon first, Yahoo as fallback
        .route(Capability::QUOTE, [Provider::Polygon, Provider::Yahoo])
        // Route fundamentals to FMP first, Yahoo as fallback
        .route(Capability::FUNDAMENTALS, [Provider::Fmp, Provider::Yahoo])
        // Route corporate (news, recommendations) to Polygon only
        .route(Capability::CORPORATE, [Provider::Polygon])
        .fetch(Fetch::Sequential)
        .build()
        .await?;
    let ticker = providers.ticker("AAPL").build().await?;
    Ok(())
}

If no .route() is set for a capability, Yahoo Finance is used by default. EDGAR is auto-injected for FILINGS when no other provider is configured.

Available Capabilities#

Capability·verified current
CapabilityConstantDescription
QuoteCapability::QUOTEPrice, volume, market cap
ChartCapability::CHARTHistorical OHLCV data
FundamentalsCapability::FUNDAMENTALSFinancial statements
CorporateCapability::CORPORATENews, recommendations, SEC metadata
OptionsCapability::OPTIONSOptions chains
DiscoveryCapability::DISCOVERYSymbol search, screeners, exchange reference data
CryptoCapability::CRYPTOCryptocurrency quotes
EconomicCapability::ECONOMICMacro series (GDP, CPI, etc.)
CalendarCapability::CALENDARMarket-wide earnings, IPO, dividend, split and economic calendars
ForexCapability::FOREXFX currency pair rates
IndicesCapability::INDICESMarket index quotes
FuturesCapability::FUTURESFutures contract quotes
CommoditiesCapability::COMMODITIESCommodity price quotes
MarketCapability::MARKETSector and industry performance, market movers
FilingsCapability::FILINGSSEC EDGAR filing data

Capabilities are bitflags — compose them with | and test membership with contains. This example runs as a real test, no network needed:

rust · runnable
use finance_query::Capability;

let market_data = Capability::QUOTE | Capability::CHART;
assert!(market_data.contains(Capability::QUOTE));
assert!(market_data.contains(Capability::CHART));
assert!(!market_data.contains(Capability::OPTIONS));
assert_eq!(Capability::QUOTE.name(), "quote");
println!("market_data = {market_data:?}");
println!("QUOTE.name() = {:?}", Capability::QUOTE.name());
recorded outputcargo soothfast docs capture
market_data = Capability(3)
QUOTE.name() = "quote"

Fetch Strategies#

Fetch controls how the provider list is queried:

Fetch·verified current
StrategyBehaviorBest for
Fetch::SequentialTry in priority order; first success wins (default)Respecting rate limits, minimizing API calls
Fetch::ParallelFire all concurrently; first success winsLowest latency for real-time data

Fetch and Provider are plain enums — constructing them never touches the network:

rust · runnable
use finance_query::{Fetch, Provider};

let strategy = Fetch::Sequential;
assert_eq!(strategy, Fetch::Sequential);
assert_ne!(Fetch::Parallel, Fetch::Sequential);

// Yahoo is the default provider; EDGAR is likewise always compiled in.
assert_eq!(Provider::default(), Provider::Yahoo);
assert_eq!(Provider::Edgar.as_str(), "edgar");
println!("default provider = {:?}", Provider::default());
println!("Edgar.as_str()   = {:?}", Provider::Edgar.as_str());
recorded outputcargo soothfast docs capture
default provider = Yahoo
Edgar.as_str()   = "edgar"
rust · no_run feature=polygon
use finance_query::{Capability, Fetch, Provider, Providers};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Sequential: try Polygon, then Yahoo if Polygon fails
    let providers = Providers::builder()
        .route(Capability::QUOTE, [Provider::Polygon, Provider::Yahoo])
        .fetch(Fetch::Sequential)
        .build()
        .await?;
    let ticker = providers.ticker("AAPL").build().await?;

    // Parallel: race Polygon against Yahoo, use whichever responds first
    let providers = Providers::builder()
        .route(Capability::QUOTE, [Provider::Polygon, Provider::Yahoo])
        .fetch(Fetch::Parallel)
        .build()
        .await?;
    let ticker = providers.ticker("AAPL").build().await?;
    Ok(())
}

Provider Capabilities Matrix#

Capabilities supported by each provider. Providers that don't support a given capability are automatically skipped during dispatch.

dispatch_selectallocations 0(limit ≤ 0)0limit 0
dispatch_selectmedian time 4 ns(limit < 100 ns)0limit 100 ns

This table is printed by the program below rather than maintained by hand, so it cannot fall behind Provider::capabilities().

rust · runnable
use finance_query::{Capability, Provider};

println!("| Provider | Capabilities |");
println!("|----------|--------------|");
for provider in Provider::all() {
    let declared: Vec<&str> = Capability::all()
        .filter(|c| provider.capabilities().contains(*c))
        .map(Capability::name)
        .collect();
    let declared = match declared.is_empty() {
        true => "none".to_string(),
        false => declared.join(", "),
    };
    println!("| `{}` | {declared} |", provider.as_str());
}
recorded outputcargo soothfast docs capture
| Provider | Capabilities |
|----------|--------------|
| `yahoo` | quote, chart, fundamentals, corporate, options, discovery, calendar, market, indices, futures, commodities |
| `polygon` | quote, chart, fundamentals, corporate, options, discovery, crypto, economic, calendar, forex, indices, futures, filings |
| `fmp` | quote, chart, fundamentals, corporate, discovery, crypto, calendar, market, forex, indices, commodities, filings |
| `alphavantage` | quote, chart, fundamentals, corporate, options, discovery, crypto, economic, calendar, market, forex, commodities, filings |
| `coingecko` | chart, discovery, crypto |
| `fred` | economic, calendar |
| `worldbank` | economic |
| `fiscaldata` | economic |
| `bls` | economic |
| `frankfurter` | forex |
| `binance` | chart, crypto |
| `kraken` | chart, crypto |
| `finra` | fundamentals |
| `defillama` | crypto |
| `gdelt` | corporate, crypto, forex |
| `cftc` | futures |
| `nasdaq` | calendar |
| `wikipedia` | indices |
| `congresstrades` | filings |
| `edgar` | corporate, discovery, filings |
| `local_market_calendar` | calendar |
| `local_exchange` | discovery |

Providers Factory (Shared Connections)#

For non-equity asset classes, use the Providers factory to create domain handles that share the same provider connections and configuration:

rust · no_run feature=full
use finance_query::{Capability, Fetch, Provider, Providers};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let providers = Providers::builder()
        .route(Capability::FOREX, [Provider::AlphaVantage])
        .route(Capability::ECONOMIC, [Provider::Fred])
        .route(Capability::CRYPTO, [Provider::CoinGecko])
        .fetch(Fetch::Sequential)
        .build()
        .await?;

    // All handles share the same provider connections
    let aapl  = providers.ticker("AAPL").logo().build().await?;   // → Ticker
    let pair  = providers.forex("USD", "EUR");                    // → ForexPair
    let btc   = providers.crypto("bitcoin");                      // → CryptoCoin
    let gdp   = providers.economic("REAL_GDP");                   // → EconomicIndicator
    let spy   = providers.index("SPY");                           // → Index
    let cl    = providers.futures("CL=F");                        // → FuturesContract
    let wheat = providers.commodity("WHEAT");                     // → Commodity
    let sec   = providers.filings("AAPL");                        // → Filings
    Ok(())
}

Four handles are market-wide rather than symbol-scoped, so their factories take no argument. market() and snapshot() are always available (movers are served keylessly from Yahoo's screeners); discovery() and calendar() require at least one of the fmp, polygon, or alphavantage features:

rust,ignore
let disco = providers.discovery();   // → Discovery: symbol search, reference data, screeners
let cal   = providers.calendar();    // → MarketCalendar: earnings/IPO/dividend/split/economic calendars
let mkt   = providers.market();      // → Market: sector performance, movers
let snap  = providers.snapshot();    // → Snapshot: cross-market watchlist snapshots
let cat   = providers.economic_catalog(); // → EconomicCatalog: find macro series (needs fred/alphavantage/polygon)

Domain Handle Methods#

HandleMethodReturns
ForexPair.quote() · .chart(interval, range) · .history(range)ForexQuote · Chart
CryptoCoin.quote(vs_currency) · .chart(vs_currency, interval, range) · .history(vs_currency, range)CryptoQuote · Chart
EconomicIndicator.series() · .as_of(date)EconomicSeries
EconomicCatalog.search(query, limit) · .categories(parent_id) · .releases()Vec<EconomicSeriesMatch> · Vec<EconomicCategory> · Vec<EconomicRelease>
Index.quote() · .chart(interval, range) · .history(range) · .constituents() · .constituent_changes()IndexQuote · Chart · Vec<IndexConstituent> · Vec<IndexConstituentChange>
FuturesContract.quote() · .chart(interval, range) · .history(range)FuturesQuote · Chart
Commodity.quote() · .chart(interval, range) · .history(range)CommodityQuote · Chart
Filings.get() · .search(query, filters) · .search_all(query, filters) · .insider_trades(limit) · .institutional_holdings() · .sections(accession, form) · .risk_factors()ProviderFilings · Vec<FilingSearchHit> · Vec<InsiderTrade> · Vec<InstitutionalHolding> · Vec<FilingSection> · Vec<RiskFactor>
Discovery.search(query, limit) · .details(symbol) · .exchanges() · .listing_status(active) · .screener(filters)Vec<SymbolMatch> · SymbolDetails · Vec<ExchangeInfo> · Vec<SymbolMatch> · Vec<ScreenerMatch>
MarketCalendar.earnings(from, to) · .ipos(..) · .dividends(..) · .splits(..) · .economic(..) · .holidays()Vec<MarketCalendarEntry>
Market.sector_performance() · .sector_performance_history(limit) · .sector_pe() · .industry_pe() · .gainers() · .losers() · .most_active()Vec<SectorPerformance> · Vec<SectorPerformanceHistory> · Vec<SectorPe> · Vec<IndustryPe> · Vec<MoverQuote>
Snapshot.get(symbols)Vec<MarketSnapshot>

Snapshot::get takes provider-spelled symbols from any market in one list ("AAPL", "X:BTCUSD", "I:SPX", "C:EURUSD", "O:NCLH221014C00005000") and answers them in a single request — one rate-limit unit instead of one per asset class. Symbols the provider cannot resolve come back as rows with error set, so the result stays aligned with the request. Polygon (max 250 symbols) is currently the only provider whose snapshot endpoint spans markets.

All chart-capable handles route through Capability::CHART (Yahoo by default) and cache per (symbol, interval, range) when .cache(ttl) is set. history(range) is sugar for chart(range.default_interval(), range). The handle's identifier is passed to the chart route as-is, so it must be a chart-route symbol (e.g. ^GSPC, NQ=F, GC=F); CryptoCoin builds "{ID}-{VS}" (e.g. "BTC-USD"), which resolves on Yahoo only for ticker-style ids.

Technical Indicators & Risk on Domain Handles#

With the indicators / risk features, every chart-capable handle also exposes the same analytics as Ticker, computed over its cached chart:

MethodFeatureReturns
.indicators(interval, range)indicatorsIndicatorsSummary
.indicator(Indicator, interval, range)indicatorsIndicatorResult
.risk(interval, range)riskRiskSummary

CryptoCoin takes a leading vs_currency argument on all three (e.g. coin.indicator(Indicator::Rsi(14), "USD", interval, range)), matching its chart().

risk() annualizes with the handle's asset-class trading calendar — 252 days for exchange-traded (index/futures/commodity), ~260 for forex, 365 for crypto (24/7) — and intraday intervals scale by session length, so Sharpe/Sortino/Calmar are correct across asset classes and intervals. beta is always None on domain handles (no benchmark is fetched).

Tickers and Providers#

Tickers supports the same multi-provider configuration as Ticker. Routing is configured through Providers::builder() and passed to Tickers via providers.tickers():

rust · no_run feature=polygon
use finance_query::{Capability, Fetch, Provider, Providers};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let providers = Providers::builder()
        .route(Capability::QUOTE, [Provider::Polygon, Provider::Yahoo])
        .fetch(Fetch::Sequential)
        .build()
        .await?;
    let tickers = providers.tickers(["AAPL", "NVDA"]).build().await?;
    Ok(())
}

note · Spark is Yahoo-only

spark() uses a Yahoo-specific batch endpoint with no equivalent in other providers. It always uses the Yahoo client regardless of provider configuration.

Provider Pages#

built with cargo soothfast docs build source