use chrono::{Utc};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use reqwest::Client;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::time::Duration;
use tracing::{debug, error};
#[derive(Debug, Clone)]
pub struct AlphaVantageConfig {
pub api_key: String,
pub timeout: Duration,
}
impl Default for AlphaVantageConfig {
fn default() -> Self {
Self {
api_key: String::new(),
timeout: Duration::from_secs(30),
}
}
}
pub struct AlphaVantageClient {
client: Client,
config: AlphaVantageConfig,
base_url: String,
rate_limiter: DefaultDirectRateLimiter,
}
impl AlphaVantageClient {
pub fn new(config: AlphaVantageConfig) -> Self {
let client = Client::builder()
.timeout(config.timeout)
.build()
.expect("Failed to create HTTP client");
let quota = Quota::per_minute(NonZeroU32::new(5).unwrap());
let rate_limiter = RateLimiter::direct(quota);
Self {
client,
config,
base_url: "https://www.alphavantage.co/query".to_string(),
rate_limiter,
}
}
pub async fn get_quote(&self, symbol: &str) -> Result<AlphaVantageQuote, AlphaVantageError> {
self.rate_limiter.until_ready().await;
let params = [
("function", "GLOBAL_QUOTE"),
("symbol", symbol),
("apikey", &self.config.api_key),
];
debug!("Alpha Vantage request: GLOBAL_QUOTE for {}", symbol);
let response = self
.client
.get(&self.base_url)
.query(¶ms)
.send()
.await?;
if response.status().is_success() {
let data: serde_json::Value = response.json().await?;
if let Some(quote) = data.get("Global Quote") {
Ok(AlphaVantageQuote {
symbol: quote.get("01. symbol")
.and_then(|v| v.as_str())
.unwrap_or(symbol)
.to_string(),
price: quote.get("05. price")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
volume: quote.get("06. volume")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(0),
change: quote.get("09. change")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
change_percent: quote.get("10. change percent")
.and_then(|v| v.as_str())
.map(|s| s.replace("%", ""))
.and_then(|s| Decimal::from_str_exact(&s).ok())
.unwrap_or_default(),
})
} else {
Err(AlphaVantageError::ApiError("No quote data found".to_string()))
}
} else {
let error_text = response.text().await.unwrap_or_default();
error!("Alpha Vantage API error: {}", error_text);
Err(AlphaVantageError::ApiError(error_text))
}
}
pub async fn get_daily(
&self,
symbol: &str,
outputsize: &str, ) -> Result<Vec<AlphaVantageBar>, AlphaVantageError> {
self.rate_limiter.until_ready().await;
let params = [
("function", "TIME_SERIES_DAILY"),
("symbol", symbol),
("outputsize", outputsize),
("apikey", &self.config.api_key),
];
let response = self
.client
.get(&self.base_url)
.query(¶ms)
.send()
.await?;
if response.status().is_success() {
let data: serde_json::Value = response.json().await?;
if let Some(time_series) = data.get("Time Series (Daily)").and_then(|v| v.as_object()) {
let mut bars = Vec::new();
for (date, values) in time_series {
if let Some(obj) = values.as_object() {
bars.push(AlphaVantageBar {
date: date.clone(),
open: obj.get("1. open")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
high: obj.get("2. high")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
low: obj.get("3. low")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
close: obj.get("4. close")
.and_then(|v| v.as_str())
.and_then(|s| Decimal::from_str_exact(s).ok())
.unwrap_or_default(),
volume: obj.get("5. volume")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(0),
});
}
}
Ok(bars)
} else {
Err(AlphaVantageError::ApiError("No time series data found".to_string()))
}
} else {
let error_text = response.text().await.unwrap_or_default();
Err(AlphaVantageError::ApiError(error_text))
}
}
pub async fn get_indicator(
&self,
symbol: &str,
indicator: &str, interval: &str, time_period: u32,
) -> Result<HashMap<String, Decimal>, AlphaVantageError> {
self.rate_limiter.until_ready().await;
let params = [
("function", indicator),
("symbol", symbol),
("interval", interval),
("time_period", &time_period.to_string()),
("series_type", "close"),
("apikey", &self.config.api_key),
];
let response = self
.client
.get(&self.base_url)
.query(¶ms)
.send()
.await?;
if response.status().is_success() {
let data: serde_json::Value = response.json().await?;
let key = format!("Technical Analysis: {}", indicator);
if let Some(indicator_data) = data.get(&key).and_then(|v| v.as_object()) {
let mut results = HashMap::new();
for (date, values) in indicator_data {
if let Some(value) = values.get(indicator).and_then(|v| v.as_str()) {
if let Ok(decimal_value) = Decimal::from_str_exact(value) {
results.insert(date.clone(), decimal_value);
}
}
}
Ok(results)
} else {
Err(AlphaVantageError::ApiError("No indicator data found".to_string()))
}
} else {
let error_text = response.text().await.unwrap_or_default();
Err(AlphaVantageError::ApiError(error_text))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlphaVantageQuote {
pub symbol: String,
pub price: Decimal,
pub volume: i64,
pub change: Decimal,
pub change_percent: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlphaVantageBar {
pub date: String,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: i64,
}
#[derive(Debug, thiserror::Error)]
pub enum AlphaVantageError {
#[error("API error: {0}")]
ApiError(String),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Parse error: {0}")]
Parse(#[from] serde_json::Error),
#[error("Rate limit exceeded")]
RateLimit,
#[error(transparent)]
Other(#[from] anyhow::Error),
}