| Crates.io | finnhub |
| lib.rs | finnhub |
| version | 0.2.1 |
| created_at | 2025-05-30 21:01:27.123582+00 |
| updated_at | 2025-06-17 20:56:21.886886+00 |
| description | A comprehensive Rust client for the Finnhub.io financial data API with 96% endpoint coverage, flexible rate limiting, and WebSocket support |
| homepage | https://github.com/jbradenbrown/finnhub |
| repository | https://github.com/jbradenbrown/finnhub |
| max_upload_size | |
| id | 1695667 |
| size | 1,004,188 |
A comprehensive Rust client for the Finnhub.io financial data API.
Add this to your Cargo.toml:
[dependencies]
finnhub = "0.2.0"
# For WebSocket support
finnhub = { version = "0.2.0", features = ["websocket"] }
use finnhub::{FinnhubClient, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Create client with your API key
let client = FinnhubClient::new("your-api-key");
// Get a stock quote
let quote = client.stock().quote("AAPL").await?;
println!("AAPL price: ${:.2}", quote.current_price);
// Get company profile
let profile = client.stock().company_profile("AAPL").await?;
println!("Company: {}", profile.name.unwrap_or_default());
// Search for symbols
let results = client.misc().symbol_search("apple", Some("US")).await?;
println!("Found {} results for 'apple'", results.count);
Ok(())
}
The library uses header authentication (X-Finnhub-Token) by default for better security. Both header and URL parameter authentication are supported by Finnhub.
use finnhub::{FinnhubClient, ClientConfig, auth::AuthMethod};
// Default: Header authentication (more secure)
let client = FinnhubClient::new("your-api-key");
// Alternative: URL parameter authentication
let config = ClientConfig {
auth_method: AuthMethod::UrlParameter,
..ClientConfig::default()
};
let client = FinnhubClient::with_config("your-api-key", config);
use finnhub::models::stock::{StatementType, StatementFrequency};
// Get financials
let financials = client.stock()
.financials("AAPL", StatementType::IncomeStatement, StatementFrequency::Annual)
.await?;
// Get insider transactions
let insiders = client.stock().insider_transactions("AAPL").await?;
// Get price target consensus
let target = client.stock().price_target("AAPL").await?;
println!("Average target: ${:.2}", target.target_mean);
Many alternative data endpoints require premium API access:
// Social sentiment (available with basic access)
let sentiment = client.stock()
.social_sentiment("AAPL", "2024-01-01", "2024-01-07")
.await?;
println!("Symbol: {}", sentiment.symbol);
println!("Total data points: {}", sentiment.data.len());
// Premium endpoints (require additional access):
// - ESG scores: client.stock().esg("AAPL")
// - Patent applications: client.stock().uspto_patents("NVDA", from, to)
// - Congressional trading: client.stock().congressional_trading("AAPL", None, None)
// - Lobbying data: client.stock().lobbying("AAPL", from, to)
// Earnings calendar
let earnings = client.calendar()
.earnings(Some("2024-01-01"), Some("2024-01-07"), None)
.await?;
println!("Upcoming earnings: {} companies", earnings.earnings_calendar.len());
// IPO calendar
let ipos = client.calendar()
.ipo("2024-01-01", "2024-01-31")
.await?;
println!("Recent IPOs: {} companies", ipos.ipo_calendar.len());
use finnhub::models::news::NewsCategory;
// Company news with sentiment
let news = client.news().company_news("AAPL", "2024-12-01", "2024-12-07").await?;
// Market-wide news
let market_news = client.news().market_news(NewsCategory::General, None).await?;
// Support and resistance levels
let levels = client.scanner().support_resistance("AAPL", "D").await?;
// Aggregate technical indicators
let indicators = client.scanner().aggregate_indicators("AAPL", "D").await?;
println!("Signal: {} (Buy: {}, Sell: {})",
indicators.technical_analysis.signal,
indicators.technical_analysis.count.buy,
indicators.technical_analysis.count.sell
);
// Symbol search
let results = client.misc().symbol_search("tesla", Some("US")).await?;
println!("Found {} results", results.count);
// Country information
let countries = client.misc().country().await?;
println!("Available in {} countries", countries.len());
// FDA calendar
let fda = client.misc().fda_calendar().await?;
println!("Upcoming FDA events: {}", fda.len());
finnhub/
├── src/
│ ├── client.rs # Main client implementation
│ ├── auth.rs # Authentication handling
│ ├── error.rs # Error types
│ ├── rate_limiter.rs # Rate limiting
│ ├── models/ # Response models
│ │ ├── stock/ # Stock models (14 modules)
│ │ ├── forex.rs # Forex models
│ │ ├── crypto.rs # Crypto models
│ │ └── ... # Other market models
│ └── endpoints/ # API endpoint implementations
│ ├── stock/ # Stock endpoints (14 modules)
│ ├── forex.rs # Forex endpoints
│ └── ... # Other endpoints
└── examples/ # Usage examples
The library provides comprehensive error handling:
use finnhub::Error;
match client.stock().quote("AAPL").await {
Ok(quote) => println!("Price: ${}", quote.current_price),
Err(Error::RateLimitExceeded { retry_after }) => {
println!("Rate limit hit, retry after {} seconds", retry_after);
},
Err(Error::Unauthorized) => {
println!("Invalid API key");
},
Err(e) => println!("Error: {}", e),
}
The client includes built-in rate limiting to comply with Finnhub's API limits:
// Default: 30 requests/second with burst capacity
let client = FinnhubClient::new("your-api-key");
// For batch processing: 15-second window (450 request burst)
let mut config = ClientConfig::default();
config.rate_limit_strategy = RateLimitStrategy::FifteenSecondWindow;
let client = FinnhubClient::with_config("your-api-key", config);
// Rate limiting is automatic
for symbol in ["AAPL", "GOOGL", "MSFT"] {
let quote = client.stock().quote(symbol).await?;
// Client automatically manages request rate
}
This library intentionally does not implement automatic retry logic, allowing applications to implement context-aware retry strategies. The library provides helpers to make this easy:
use finnhub::{Error, Result};
use std::time::Duration;
use tokio::time::sleep;
async fn with_retry<T, F, Fut>(mut f: F, max_attempts: u32) -> Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut attempt = 0;
loop {
attempt += 1;
match f().await {
Ok(result) => return Ok(result),
Err(e) if e.is_retryable() && attempt < max_attempts => {
let delay = e.retry_after()
.unwrap_or(1) // Default 1 second
.max(1); // At least 1 second
sleep(Duration::from_secs(delay)).await;
continue;
}
Err(e) => return Err(e),
}
}
}
// Usage
let quote = with_retry(|| client.stock().quote("AAPL"), 3).await?;
Response caching is best implemented at the application layer where you understand data freshness requirements:
use finnhub::models::stock::Quote;
use std::collections::HashMap;
use std::time::{Duration, Instant};
struct CachedQuote {
quote: Quote,
fetched_at: Instant,
}
struct QuoteCache {
cache: HashMap<String, CachedQuote>,
ttl: Duration,
}
impl QuoteCache {
async fn get_quote(&mut self, client: &FinnhubClient, symbol: &str) -> Result<Quote> {
if let Some(cached) = self.cache.get(symbol) {
if cached.fetched_at.elapsed() < self.ttl {
return Ok(cached.quote.clone());
}
}
let quote = client.stock().quote(symbol).await?;
self.cache.insert(symbol.to_string(), CachedQuote {
quote: quote.clone(),
fetched_at: Instant::now(),
});
Ok(quote)
}
}
Always handle specific error types appropriately:
match client.stock().quote("AAPL").await {
Ok(quote) => process_quote(quote),
Err(Error::RateLimitExceeded { retry_after }) => {
// Back off and retry later
sleep(Duration::from_secs(retry_after)).await;
}
Err(Error::Unauthorized) => {
// Check API key configuration
panic!("Invalid API key");
}
Err(e) => {
// Log and handle other errors
eprintln!("API error: {}", e);
}
}
When making multiple requests, consider rate limits and use concurrency control:
use futures::stream::{self, StreamExt};
let symbols = vec!["AAPL", "GOOGL", "MSFT", "AMZN", "FB"];
// Process 3 symbols concurrently to stay well under rate limit
let quotes: Vec<_> = stream::iter(symbols)
.map(|symbol| async move {
client.stock().quote(symbol).await
})
.buffer_unordered(3)
.collect()
.await;
Basic WebSocket structure is implemented but requires significant work:
// Requires 'websocket' feature
use finnhub::websocket::{WebSocketClient, WebSocketMessage};
let client = WebSocketClient::new("your-api-key");
let mut stream = client.connect().await?;
// Subscribe to symbols
stream.subscribe("AAPL").await?;
// Process messages
match stream.next().await? {
Some(WebSocketMessage::Trade { data }) => {
for trade in data {
println!("Trade: {} @ ${}", trade.symbol, trade.price);
}
}
Some(WebSocketMessage::Ping) => {
println!("Received ping");
}
Some(WebSocketMessage::Error { msg }) => {
eprintln!("Error: {}", msg);
}
None => println!("Stream closed"),
}
See examples/websocket_basic.rs for a complete example.
Note: WebSocket support is minimal and not recommended for production use. It lacks:
For examples and tests, you can use environment variables:
# .env file
FINNHUB_API_KEY=your_api_key_here
// In your code
dotenv::dotenv().ok();
let api_key = std::env::var("FINNHUB_API_KEY")
.expect("FINNHUB_API_KEY must be set");
Enable debug logging to see request details:
RUST_LOG=finnhub=debug cargo run
Contributions are welcome! Please feel free to submit a Pull Request. See CLAUDE.md for development guidelines and architecture details.
# Clone the repository
git clone https://github.com/jbradenbrown/finnhub
cd finnhub
# Run tests (requires API key)
FINNHUB_API_KEY=your_key cargo test
# Run specific example
FINNHUB_API_KEY=your_key cargo run --example basic_usage
# Check formatting and lints
cargo fmt -- --check
cargo clippy -- -D warnings
This library was developed with assistance from Claude, Anthropic's AI assistant, using the Claude Code development environment. The AI helped with implementation, documentation, and best practices while maintaining human oversight and decision-making throughout the development process.
Licensed under either of:
at your option.