borsa-yfinance

Crates.ioborsa-yfinance
lib.rsborsa-yfinance
version0.2.0
created_at2025-10-18 19:35:52.909568+00
updated_at2025-10-23 14:29:37.800853+00
descriptionYahoo Finance API connector for the borsa financial data ecosystem with comprehensive market data and streaming support.
homepagehttps://github.com/borsaorg/borsa
repositoryhttps://github.com/borsaorg/borsa
max_upload_size
id1889564
size210,605
G. Ramistella (gramistella)

documentation

https://docs.rs/borsa-yfinance

README

borsa-yfinance

Yahoo Finance connector for the borsa ecosystem. This crate is both a ready-to-use provider and a reference implementation for building custom connectors.

Crates.io Docs.rs Downloads License

Overview

borsa-yfinance implements borsa-core::BorsaConnector using yfinance-rs under the hood. It covers a wide set of capabilities: quotes, history, search, profile, fundamentals, options, analysis, holders, sustainability, and news, and can be used as a reference when building a connector.

Use it directly, or follow its patterns to build your own connector.

Install

[dependencies]
borsa-yfinance = "0.2.0"
borsa-core = "0.2.0"

Quick start

use borsa_yfinance::YfConnector;
use borsa_core::{connector::QuoteProvider, AssetKind, Instrument};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let yf = Arc::new(YfConnector::new_default());
    let aapl = Instrument::from_symbol("AAPL", AssetKind::Equity)?;
    let q = yf.quote(&aapl).await?;
    if let Some(price) = &q.price {
        println!("{} price: {}", q.symbol.as_str(), price.format());
    }
    Ok(())
}

Using YfConnector in the router

Observability

Enable the tracing feature to emit spans for all public provider endpoints (quotes, history, search, profile, fundamentals, options, analysis, holders, ESG, news, streaming):

[dependencies]
borsa-yfinance = { version = "0.2", features = ["tracing"] }

Run with the example subscriber setup:

RUST_LOG=info,borsa=trace,borsa_yfinance=trace \
  cargo run -p borsa-examples --example 00_tracing \
  --features "borsa/tracing borsa-yfinance/tracing"
use borsa::{Borsa};
use borsa_yfinance::YfConnector;
use borsa_core::{connector::QuoteProvider, AssetKind, Currency, Instrument, Money, Symbol};
use std::sync::Arc;

let yf = Arc::new(YfConnector::new_default());
let borsa = Borsa::builder().with_connector(yf).build()?;
let inst = Instrument::from_symbol("MSFT", AssetKind::Equity)?;
let quote = borsa.quote(&inst).await?;

Designing a connector: the YF blueprint

  1. Define a small set of adapter traits to wrap the SDK
#[async_trait]
pub trait YfQuotes { async fn fetch(&self, symbols: &[String]) -> Result<Vec<yf::core::Quote>, BorsaError>; }
#[async_trait]
pub trait YfHistory { async fn fetch_full(&self, symbol: &str, req: yf::core::services::HistoryRequest) -> Result<yf::HistoryResponse, BorsaError>; }
// ... YfSearch, YfProfile, YfFundamentals, YfOptions, YfAnalysis, YfHolders, YfEsg, YfNews
  1. Provide an adapter that holds the client once and implements all adapters
#[derive(Clone)]
pub struct RealAdapter { client: yf::YfClient }
impl RealAdapter { pub fn new_default() -> Self { Self { client: yf::YfClient::default() } } }
  1. Expose test adapters via closures so unit tests don’t need network access
impl dyn YfQuotes { pub fn from_fn<F>(f: F) -> Arc<dyn YfQuotes> where F: Send + Sync + 'static + Fn(Vec<String>) -> Result<Vec<yf::core::Quote>, BorsaError> { /* ... */ } }
  1. Return the native paft types (Symbol, Money, domain enums) directly from adapters.

  2. Delegate capability traits and advertise them via BorsaConnector::as_*_provider.

#[async_trait]
impl QuoteProvider for YfConnector {
    async fn quote(&self, instrument: &Instrument) -> Result<Quote, BorsaError> {
        // call adapter + normalise errors
    }
}

impl BorsaConnector for YfConnector {
    fn name(&self) -> &'static str { "borsa-yfinance" }

    fn as_quote_provider(&self) -> Option<&dyn QuoteProvider> {
        Some(self)
    }
    // advertise other capabilities similarly
}

Capability matrix

This connector advertises and implements the following capabilities:

  • Quotes, History, Search, Profile
  • Fundamentals (earnings, statements), Options
  • Analysis (recommendations, price targets), Holders
  • ESG (sustainability scores), News

History intervals

Native intervals returned by supported_history_intervals:

  • 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1d, 5d, 1w, 1mo, 3mo

The orchestrator may resample as needed (e.g., auto-subdaily->daily, weekly).

Error mapping

Errors from yfinance-rs are converted to BorsaError::connector("borsa-yfinance", message) to provide consistent, debuggable failures in multi-provider flows. Missing symbols surface as BorsaError::NotFound{ .. } where relevant via router logic.

Testing strategy

  • Unit tests use closure-based test adapters to inject precise responses and errors
  • Conversion tests validate field-by-field mapping is stable
  • Capability tests ensure flags correctly reflect implemented adapters

Run:

cargo test -p borsa-yfinance | cat

Contributing guidelines for connector authors

  • Keep the public connector small; put IO and state in an adapter layer
  • Surface exact native intervals; leave planning/resampling to the router
  • Implement only supported endpoints and let defaults return unsupported
  • Prefer deterministic, pure conversions; avoid IO in mapping code
  • Accurately reflect capabilities; the router depends on them for routing
  • Provide test adapters so contributors can write focused tests without network

Building the connector for testability (patterns and examples)

Feature flag: the lightweight adapter helpers (CloneArcAdapters, YfQuotes::from_fn, etc.) are gated behind the optional test-adapters feature. Enable it in Cargo.toml (for example, borsa-yfinance = { version = "x.y", features = ["test-adapters"] }) or on the command line with cargo test --features borsa-yfinance/test-adapters.

The design here intentionally separates the public connector from IO so you can write fast, deterministic tests with zero network.

Pattern A: Quotes-only unit test (no router)

use std::sync::Arc;
use borsa_yfinance::YfConnector;
use borsa_yfinance::adapter::{CloneArcAdapters, YfQuotes};
use borsa_core::{connector::QuoteProvider, AssetKind, Currency, Instrument, Money, Symbol};

// 1) Create a minimal adapter that exposes only quotes
struct QuotesOnlyAdapter { quotes: Arc<dyn YfQuotes> }
impl CloneArcAdapters for QuotesOnlyAdapter {
    fn clone_arc_quotes(&self) -> Arc<dyn YfQuotes> { self.quotes.clone() }
}

#[tokio::test]
async fn quote_smoke_test() {
    // 2) Provide a closure-based quote implementation (no network)
    let quotes = <dyn YfQuotes>::from_fn(|symbols| {
        assert_eq!(symbols, vec!["AAPL".to_string()]);
        let price = Money::from_canonical_str(
            "190.0",
            Currency::Iso(borsa_core::IsoCurrency::USD),
        )
        .unwrap();
        let previous = Money::from_canonical_str(
            "189.5",
            Currency::Iso(borsa_core::IsoCurrency::USD),
        )
        .unwrap();
        Ok(vec![yfinance_rs::core::Quote {
            symbol: Symbol::new("AAPL").unwrap(),
            shortname: Some("Apple".into()),
            price: Some(price),
            previous_close: Some(previous),
            exchange: None,
            market_state: None,
        }])
    });

    // 3) Build the connector from the adapter
    let yf = YfConnector::from_adapter(QuotesOnlyAdapter { quotes });

    // 4) Exercise the API
    let aapl = Instrument::from_symbol("AAPL", AssetKind::Equity).unwrap();
    let q = yf.quote(&aapl).await.unwrap();
    assert_eq!(q.symbol.as_str(), "AAPL");
    assert_eq!(q.price.unwrap().format(), "190.0 USD");
}

Pattern B: Search unit test using the adapter helpers

use std::sync::Arc;
use borsa_yfinance::YfConnector;
use borsa_yfinance::adapter::{CloneArcAdapters, YfSearch};
use borsa_core::{AssetKind, SearchRequest, SearchResponse, SearchResult, Symbol};

struct SearchOnlyAdapter { search: Arc<dyn YfSearch> }
impl CloneArcAdapters for SearchOnlyAdapter {
    fn clone_arc_search(&self) -> Arc<dyn YfSearch> { self.search.clone() }
}

#[tokio::test]
async fn search_returns_symbols() {
    let search = <dyn YfSearch>::from_fn(|query| {
        assert_eq!(query, "Apple");
        Ok(SearchResponse {
            results: vec![
                SearchResult {
                    symbol: Symbol::new("AAPL").unwrap(),
                    name: Some("Apple Inc.".into()),
                    exchange: None,
                    kind: AssetKind::Equity,
                },
                SearchResult {
                    symbol: Symbol::new("APPL34").unwrap(),
                    name: Some("Apple BDR".into()),
                    exchange: None,
                    kind: AssetKind::Equity,
                },
            ],
        })
    });

    let yf = YfConnector::from_adapter(SearchOnlyAdapter { search });
    let res = yf.search(SearchRequest::new("Apple").with_limit(2)).await.unwrap();
    assert_eq!(res.results.len(), 2);
    assert_eq!(res.results[0].symbol.as_str(), "AAPL");
}

Pattern C: End-to-end router test with injected YF

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

struct QuotesOnlyAdapter { quotes: Arc<dyn YfQuotes> }
impl CloneArcAdapters for QuotesOnlyAdapter {
    fn clone_arc_quotes(&self) -> Arc<dyn YfQuotes> { self.quotes.clone() }
}

#[tokio::test]
async fn router_uses_injected_yf() {
    let quotes = <dyn YfQuotes>::from_fn(|symbols| Ok(vec![yfinance_rs::core::Quote {
        symbol: symbols[0].clone(),
        shortname: None,
        regular_market_price: Some(123.45),
        regular_market_previous_close: None,
        currency: None,
        exchange: None,
        market_state: None,
    }]))
    ;

    let yf = Arc::new(YfConnector::from_adapter(QuotesOnlyAdapter { quotes }));
    let borsa = Borsa::builder().with_connector(yf).build()?;

    let inst = Instrument::new("MSFT", AssetKind::Equity);
    let q = borsa.quote(&inst).await.unwrap();
    assert_eq!(q.symbol, "MSFT");
}

Notes:

  • Only override the adapter(s) you need in a test; the rest fall back to clear unsupported defaults.
  • Keep conversions in convert.rs pure so they can be fuzzed and unit-tested in isolation.
  • For history tests, use HistoryRequest::from_range(range, interval) and provide a synthetic yfinance_rs::HistoryResponse through YfHistory::from_fn.

License

MIT — see LICENSE

Disclaimer

This crate provides access to Yahoo Finance data. Ensure compliance with Yahoo’s terms of service.

Commit count: 0

cargo fmt