
# borsa

[![Crates.io](https://img.shields.io/crates/v/borsa)](https://crates.io/crates/borsa)
[![Docs.rs](https://docs.rs/borsa/badge.svg)](https://docs.rs/borsa)
[![Downloads](https://img.shields.io/crates/d/borsa)](https://crates.io/crates/borsa)
[![License](https://img.shields.io/crates/l/borsa)](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`:

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

## Usage

### Fetch your first quote

```rust
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
## Observability (optional)

Enable the `tracing` feature to emit structured spans from router entry points and core orchestration helpers:

```toml
[dependencies]
borsa = { version = "0.1", features = ["tracing"] }
```

Initialize a subscriber in your application:

```rust
tracing_subscriber::fmt()
    .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
    .init();
```

See the top-level `examples/examples/00_tracing.rs` for a working end-to-end sample.

### 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:

```rust
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:

```rust
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

```rust
// 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

```rust
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

```rust
// 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

```rust
// 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

```rust
// 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

```rust
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`](https://github.com/paft-rs/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`:

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

Example:

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
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](https://github.com/borsaorg/borsa/blob/main/examples/examples/) 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](https://github.com/borsaorg/borsa/blob/main/CONTRIBUTING.md) and our [Code of Conduct](https://github.com/borsaorg/borsa/blob/main/CODE_OF_CONDUCT.md).

### Building from Source

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

### Running Tests

```bash
cargo test --workspace
```

### Running Examples

```bash
cd examples
cargo run --example 01_simple_quote
```

## License

This project is licensed under the MIT License - see the [LICENSE](https://github.com/borsaorg/borsa/blob/main/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!
