borsa 0.1.0

High-level, pluggable market data API for Rust with multi-provider support.
Documentation

borsa

Crates.io Docs.rs Downloads License

The unified, intelligent, and resilient financial data toolkit for Rust.

Overview

borsa provides a high-level, asynchronous API for fetching market and financial data from multiple sources. Instead of juggling different client libraries and data formats, borsa offers a single, consistent interface that intelligently routes requests across multiple data providers.

Features

  • Pluggable Architecture: Add multiple data connectors and let borsa automatically choose the best one
  • Intelligent Fallback: If one provider fails, automatically try the next one
  • Smart Data Merging: Combine data from multiple sources for more complete datasets
  • High Performance: Async/await with efficient concurrent requests
  • Asset-Specific Routing: Configure different providers for different asset types
  • Rich Data Types: Quotes, historical data, fundamentals, options, news, and more

Installation

Add borsa and a connector to your Cargo.toml:

[dependencies]
borsa = "0.1.0"
borsa-yfinance = "0.1.0"
tokio = { version = "1", features = ["full"] }

Usage

Fetch your first quote

use borsa::Borsa;
use borsa_core::{AssetKind, Instrument};
use borsa_yfinance::YfConnector;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a connector and build the client
    let yf = Arc::new(YfConnector::new_default());
    let borsa = Borsa::builder().with_connector(yf).build()?;

    // Define the instrument (validated + uppercased automatically)
    let aapl = Instrument::from_symbol("AAPL", AssetKind::Equity)?;

    // Fetch the quote
    let quote = borsa.quote(&aapl).await?;
    if let Some(price) = &quote.price {
        println!("{} last: {}", quote.symbol.as_str(), price.format());
    }

    Ok(())
}

Concepts

Connectors

Connectors are plugins that fetch data from specific providers. borsa comes with:

  • borsa-yfinance: Yahoo Finance connector (free, no API key required)

Instruments

An Instrument represents a financial asset:

use borsa_core::{AssetKind, Instrument};

// Stocks
let aapl = Instrument::from_symbol("AAPL", AssetKind::Equity)
    .expect("valid symbol");
let tsla = Instrument::from_symbol("TSLA", AssetKind::Equity)
    .expect("valid symbol");

// Cryptocurrencies
let btc = Instrument::from_symbol("BTC-USD", AssetKind::Crypto)
    .expect("valid symbol");

// ETFs
let spy = Instrument::from_symbol("SPY", AssetKind::Equity)
    .expect("valid symbol");

Priority Configuration

Configure which connectors to use for different assets:

let borsa = Borsa::builder()
    .with_connector(yf_connector.clone())
    .with_connector(alpha_vantage_connector.clone())
    // Prefer Alpha Vantage for crypto (type-safe, ergonomic API)
    .prefer_for_kind(AssetKind::Crypto, &[alpha_vantage_connector.clone(), yf_connector.clone()])
    // Use specific connector for TSLA
    .prefer_symbol("TSLA", &[alpha_vantage_connector, yf_connector])
    .build()?;

Data Types

Quotes & Market Data

// Get live quote
let quote = borsa.quote(&aapl).await?;
if let Some(price) = &quote.price {
    println!("{} price: {}", quote.symbol.as_str(), price.format());
}

// Get comprehensive info (snapshot + warnings)
let report = borsa.info(&aapl).await?;
if let Some(info) = report.info {
    if let Some(price) = &info.last {
        println!("{} last: {}", info.symbol.as_str(), price.format());
    }
    println!("Market state: {}", info.market_state.map_or("N/A".into(), |s| s.to_string()));
    println!("Warnings: {}", report.warnings.join(", "));
}

Historical Data

use borsa_core::{HistoryRequest, Range, Interval};

// Get 6 months of daily data
let req = HistoryRequest::try_from_range(Range::M6, Interval::D1)?;
let history = borsa.history(&aapl, req.clone()).await?;

println!("Fetched {} candles", history.candles.len());
if let Some(last) = history.candles.last() {
    println!("Latest close: {}", last.close.format());
}

// Get data with attribution (see which connector provided each piece)
let (history, attribution) = borsa.history_with_attribution(&aapl, req).await?;
for (connector, span) in attribution.spans {
    println!("{}: {} -> {}", connector, span.start, span.end);
}

Fundamentals

// Income Statement
let income = borsa.income_statement(&aapl, true).await?; // quarterly
if let Some(latest) = income.first() {
    if let Some(revenue) = &latest.total_revenue {
        println!("Revenue: {}", revenue.format());
    }
}

// Balance Sheet
let balance = borsa.balance_sheet(&aapl, true).await?;
if let Some(latest) = balance.first() {
    if let Some(assets) = &latest.total_assets {
        println!("Total Assets: {}", assets.format());
    }
}

// Cash Flow
let cashflow = borsa.cashflow(&aapl, true).await?;
if let Some(latest) = cashflow.first() {
    if let Some(fcf) = &latest.free_cash_flow {
        println!("Free Cash Flow: {}", fcf.format());
    }
}

Options Data

// Get available expiration dates
let expirations = borsa.options_expirations(&aapl).await?;
println!("Found {} expiration dates", expirations.len());

// Get option chain for nearest expiration
if let Some(&next_expiry) = expirations.first() {
    let chain = borsa.option_chain(&aapl, Some(next_expiry)).await?;
    println!("Calls: {}, Puts: {}", chain.calls.len(), chain.puts.len());
}

Analysis & Recommendations

// Analyst recommendations
let recs = borsa.recommendations(&aapl).await?;
let summary = borsa.recommendations_summary(&aapl).await?;
println!("Mean recommendation: {}", summary.mean.unwrap_or(0.0));

// Price targets
let target = borsa.analyst_price_target(&aapl).await?;
let mean = target.mean.as_ref().map(|m| m.format()).unwrap_or_else(|| "N/A".into());
let low = target.low.as_ref().map(|m| m.format()).unwrap_or_else(|| "N/A".into());
let high = target.high.as_ref().map(|m| m.format()).unwrap_or_else(|| "N/A".into());
println!("Target: {mean} (low: {low}, high: {high})");

News & Events

use borsa_core::types::NewsRequest;

// Get recent news
let news_req = NewsRequest::default();
let news = borsa.news(&aapl, news_req).await?;
for article in news.iter().take(5) {
    println!("{}: {}", article.title, article.publisher.as_deref().unwrap_or(""));
}

// Get upcoming events
let calendar = borsa.calendar(&aapl).await?;
for ts in calendar.earnings_dates.iter().take(3) {
    println!("Earnings at: {}", ts);
}

DataFrames (PAFT integration)

borsa builds on paft. Enabling the dataframe feature on borsa activates paft's Polars integration, allowing you to call .to_dataframe() on returned types.

Enable it in your Cargo.toml:

[dependencies]
borsa = { version = "0.1", features = ["dataframe"] }

Example:

use borsa_core::ToDataFrame; // bring the extension trait into scope

let quote = borsa.quote(&aapl).await?;
let df = quote.to_dataframe()?; // -> polars::DataFrame

Advanced Features

Bulk Operations

Download data for multiple instruments efficiently:

let instruments = [
    Instrument::from_symbol("AAPL", AssetKind::Equity).expect("valid symbol"),
    Instrument::from_symbol("GOOGL", AssetKind::Equity).expect("valid symbol"),
    Instrument::from_symbol("MSFT", AssetKind::Equity).expect("valid symbol"),
];

let summary = borsa.download()
    .instruments(&instruments)?
    .range(Range::Y1)
    .interval(Interval::D1)
    .run()
    .await?;

if let Some(report) = summary.response {
    for (symbol, history) in report.history {
        println!("{}: {} candles", symbol.as_str(), history.candles.len());
    }
}

Automatic Resampling

Configure automatic data resampling:

use borsa::Resampling;

let borsa = Borsa::builder()
    .with_connector(yf_connector)
    // Always convert to daily bars
    .resampling(Resampling::Daily)
    // Or convert to weekly bars
    // .resampling(Resampling::Weekly)
    .build()?;

History Merge Strategy

Control how historical data is fetched from multiple providers:

use borsa::MergeStrategy;

let borsa = Borsa::builder()
    .with_connector(yf_connector)
    .with_connector(alpha_vantage_connector)
    // Deep merge: fetch from all providers and merge data (default)
    .merge_history_strategy(MergeStrategy::Deep)
    // Or fallback: stop at first provider with data (more economical)
    // .merge_history_strategy(MergeStrategy::Fallback)
    .build()?;

Merge Strategies:

  • Deep (default): Fetches from all eligible providers concurrently and merges their data. This produces the most complete dataset by backfilling gaps from lower-priority providers, but uses more API calls.
  • Fallback: Iterates through providers sequentially and stops as soon as one returns a non-empty dataset. This is more economical for API rate limits but may miss data from lower-priority providers.

Multi-Quote Fetching

Get quotes for multiple instruments efficiently:

let instruments = [
    Instrument::from_symbol("AAPL", AssetKind::Equity).expect("valid symbol"),
    Instrument::from_symbol("GOOGL", AssetKind::Equity).expect("valid symbol"),
    Instrument::from_symbol("BTC-USD", AssetKind::Crypto).expect("valid symbol"),
];

let (quotes, failures) = borsa.quotes(&instruments).await?;
for q in quotes {
    if let Some(price) = &q.price {
        println!("{}: {}", q.symbol.as_str(), price.format());
    }
}
if !failures.is_empty() {
    for (inst, err) in failures {
        eprintln!("failed: {} -> {}", inst.symbol().as_str(), err);
    }
}

Architecture

The Borsa Ecosystem

  • borsa: High-level client library (this crate)
  • borsa-core: Core traits and types for building connectors
  • borsa-yfinance: Yahoo Finance connector

Building Custom Connectors

Create your own connector by implementing role traits and advertising them via the BorsaConnector capability accessors. A minimal, current skeleton:

use async_trait::async_trait;
use borsa_core::{
    connector::{BorsaConnector, QuoteProvider, HistoryProvider},
    types::{AssetKind, Instrument, Interval, HistoryRequest, HistoryResponse, Quote},
    BorsaError,
};

pub struct MyConnector;

#[async_trait]
impl QuoteProvider for MyConnector {
    async fn quote(&self, inst: &Instrument) -> Result<Quote, BorsaError> {
        // fetch + map to Quote
        Err(BorsaError::unsupported("quote"))
    }
}

#[async_trait]
impl HistoryProvider for MyConnector {
    async fn history(
        &self,
        _inst: &Instrument,
        _req: HistoryRequest,
    ) -> Result<HistoryResponse, BorsaError> {
        Err(BorsaError::unsupported("history"))
    }

    fn supported_history_intervals(&self, _kind: AssetKind) -> &'static [Interval] { &[] }
}

impl BorsaConnector for MyConnector {
    fn name(&self) -> &'static str { "my-connector" }
    fn vendor(&self) -> &'static str { "My Vendor" }
    fn supports_kind(&self, kind: AssetKind) -> bool { matches!(kind, AssetKind::Equity) }

    fn as_quote_provider(&self) -> Option<&dyn QuoteProvider> { Some(self) }
    fn as_history_provider(&self) -> Option<&dyn HistoryProvider> { Some(self) }
}

Examples

Check out the examples package for comprehensive usage examples:

  • 01_simple_quote.rs - Basic quote fetching
  • 02_history_merge.rs - Historical data with multiple sources
  • 03_search.rs - Symbol search
  • 04_price_target.rs - Analyst price target
  • 05_options_chain.rs - Options data
  • 06_fundamentals_deep_dive.rs - Financial statements
  • 07_financial_snapshot.rs - Aggregated info snapshot
  • 08_history_resampling.rs - Resampling and cadence control
  • 09_stock_comparison.rs - Multi-symbol comparison
  • 10_analyst_recommendations.rs - Analyst data
  • 11_upcoming_events.rs - Calendar/events
  • 12_per_symbol_priority.rs - Per-symbol provider priority

Contributing

We welcome contributions! Please see our Contributing Guide and our Code of Conduct.

Building from Source

git clone https://github.com/borsaorg/borsa.git
cd borsa
cargo build --workspace

Running Tests

cargo test --workspace

Running Examples

cd examples
cargo run --example 01_simple_quote

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Yahoo Finance for providing free market data
  • The Rust community for building amazing async tools

Ready to build something amazing with financial data? Start with borsa today!