nt_execution/
alpha_vantage.rs

1// Alpha Vantage market data integration
2//
3// Free API for market data (500 requests/day free tier)
4// Features:
5// - Stock quotes and historical data
6// - Technical indicators (50+ indicators)
7// - Fundamental data
8// - Forex and crypto data
9
10use chrono::{Utc};
11use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
12use reqwest::Client;
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::num::NonZeroU32;
17use std::time::Duration;
18use tracing::{debug, error};
19
20/// Alpha Vantage configuration
21#[derive(Debug, Clone)]
22pub struct AlphaVantageConfig {
23    /// API key from alphavantage.co
24    pub api_key: String,
25    /// Request timeout
26    pub timeout: Duration,
27}
28
29impl Default for AlphaVantageConfig {
30    fn default() -> Self {
31        Self {
32            api_key: String::new(),
33            timeout: Duration::from_secs(30),
34        }
35    }
36}
37
38/// Alpha Vantage client for market data
39pub struct AlphaVantageClient {
40    client: Client,
41    config: AlphaVantageConfig,
42    base_url: String,
43    rate_limiter: DefaultDirectRateLimiter,
44}
45
46impl AlphaVantageClient {
47    /// Create a new Alpha Vantage client
48    pub fn new(config: AlphaVantageConfig) -> Self {
49        let client = Client::builder()
50            .timeout(config.timeout)
51            .build()
52            .expect("Failed to create HTTP client");
53
54        // Free tier: 5 requests per minute, 500 per day
55        let quota = Quota::per_minute(NonZeroU32::new(5).unwrap());
56        let rate_limiter = RateLimiter::direct(quota);
57
58        Self {
59            client,
60            config,
61            base_url: "https://www.alphavantage.co/query".to_string(),
62            rate_limiter,
63        }
64    }
65
66    /// Get real-time quote for a symbol
67    pub async fn get_quote(&self, symbol: &str) -> Result<AlphaVantageQuote, AlphaVantageError> {
68        self.rate_limiter.until_ready().await;
69
70        let params = [
71            ("function", "GLOBAL_QUOTE"),
72            ("symbol", symbol),
73            ("apikey", &self.config.api_key),
74        ];
75
76        debug!("Alpha Vantage request: GLOBAL_QUOTE for {}", symbol);
77
78        let response = self
79            .client
80            .get(&self.base_url)
81            .query(&params)
82            .send()
83            .await?;
84
85        if response.status().is_success() {
86            let data: serde_json::Value = response.json().await?;
87
88            if let Some(quote) = data.get("Global Quote") {
89                Ok(AlphaVantageQuote {
90                    symbol: quote.get("01. symbol")
91                        .and_then(|v| v.as_str())
92                        .unwrap_or(symbol)
93                        .to_string(),
94                    price: quote.get("05. price")
95                        .and_then(|v| v.as_str())
96                        .and_then(|s| Decimal::from_str_exact(s).ok())
97                        .unwrap_or_default(),
98                    volume: quote.get("06. volume")
99                        .and_then(|v| v.as_str())
100                        .and_then(|s| s.parse().ok())
101                        .unwrap_or(0),
102                    change: quote.get("09. change")
103                        .and_then(|v| v.as_str())
104                        .and_then(|s| Decimal::from_str_exact(s).ok())
105                        .unwrap_or_default(),
106                    change_percent: quote.get("10. change percent")
107                        .and_then(|v| v.as_str())
108                        .map(|s| s.replace("%", ""))
109                        .and_then(|s| Decimal::from_str_exact(&s).ok())
110                        .unwrap_or_default(),
111                })
112            } else {
113                Err(AlphaVantageError::ApiError("No quote data found".to_string()))
114            }
115        } else {
116            let error_text = response.text().await.unwrap_or_default();
117            error!("Alpha Vantage API error: {}", error_text);
118            Err(AlphaVantageError::ApiError(error_text))
119        }
120    }
121
122    /// Get daily time series data
123    pub async fn get_daily(
124        &self,
125        symbol: &str,
126        outputsize: &str, // "compact" (100 days) or "full"
127    ) -> Result<Vec<AlphaVantageBar>, AlphaVantageError> {
128        self.rate_limiter.until_ready().await;
129
130        let params = [
131            ("function", "TIME_SERIES_DAILY"),
132            ("symbol", symbol),
133            ("outputsize", outputsize),
134            ("apikey", &self.config.api_key),
135        ];
136
137        let response = self
138            .client
139            .get(&self.base_url)
140            .query(&params)
141            .send()
142            .await?;
143
144        if response.status().is_success() {
145            let data: serde_json::Value = response.json().await?;
146
147            if let Some(time_series) = data.get("Time Series (Daily)").and_then(|v| v.as_object()) {
148                let mut bars = Vec::new();
149
150                for (date, values) in time_series {
151                    if let Some(obj) = values.as_object() {
152                        bars.push(AlphaVantageBar {
153                            date: date.clone(),
154                            open: obj.get("1. open")
155                                .and_then(|v| v.as_str())
156                                .and_then(|s| Decimal::from_str_exact(s).ok())
157                                .unwrap_or_default(),
158                            high: obj.get("2. high")
159                                .and_then(|v| v.as_str())
160                                .and_then(|s| Decimal::from_str_exact(s).ok())
161                                .unwrap_or_default(),
162                            low: obj.get("3. low")
163                                .and_then(|v| v.as_str())
164                                .and_then(|s| Decimal::from_str_exact(s).ok())
165                                .unwrap_or_default(),
166                            close: obj.get("4. close")
167                                .and_then(|v| v.as_str())
168                                .and_then(|s| Decimal::from_str_exact(s).ok())
169                                .unwrap_or_default(),
170                            volume: obj.get("5. volume")
171                                .and_then(|v| v.as_str())
172                                .and_then(|s| s.parse().ok())
173                                .unwrap_or(0),
174                        });
175                    }
176                }
177
178                Ok(bars)
179            } else {
180                Err(AlphaVantageError::ApiError("No time series data found".to_string()))
181            }
182        } else {
183            let error_text = response.text().await.unwrap_or_default();
184            Err(AlphaVantageError::ApiError(error_text))
185        }
186    }
187
188    /// Get technical indicator (SMA, EMA, RSI, etc.)
189    pub async fn get_indicator(
190        &self,
191        symbol: &str,
192        indicator: &str, // SMA, EMA, RSI, MACD, etc.
193        interval: &str,  // 1min, 5min, 15min, 30min, 60min, daily, weekly, monthly
194        time_period: u32,
195    ) -> Result<HashMap<String, Decimal>, AlphaVantageError> {
196        self.rate_limiter.until_ready().await;
197
198        let params = [
199            ("function", indicator),
200            ("symbol", symbol),
201            ("interval", interval),
202            ("time_period", &time_period.to_string()),
203            ("series_type", "close"),
204            ("apikey", &self.config.api_key),
205        ];
206
207        let response = self
208            .client
209            .get(&self.base_url)
210            .query(&params)
211            .send()
212            .await?;
213
214        if response.status().is_success() {
215            let data: serde_json::Value = response.json().await?;
216
217            let key = format!("Technical Analysis: {}", indicator);
218            if let Some(indicator_data) = data.get(&key).and_then(|v| v.as_object()) {
219                let mut results = HashMap::new();
220
221                for (date, values) in indicator_data {
222                    if let Some(value) = values.get(indicator).and_then(|v| v.as_str()) {
223                        if let Ok(decimal_value) = Decimal::from_str_exact(value) {
224                            results.insert(date.clone(), decimal_value);
225                        }
226                    }
227                }
228
229                Ok(results)
230            } else {
231                Err(AlphaVantageError::ApiError("No indicator data found".to_string()))
232            }
233        } else {
234            let error_text = response.text().await.unwrap_or_default();
235            Err(AlphaVantageError::ApiError(error_text))
236        }
237    }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct AlphaVantageQuote {
242    pub symbol: String,
243    pub price: Decimal,
244    pub volume: i64,
245    pub change: Decimal,
246    pub change_percent: Decimal,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AlphaVantageBar {
251    pub date: String,
252    pub open: Decimal,
253    pub high: Decimal,
254    pub low: Decimal,
255    pub close: Decimal,
256    pub volume: i64,
257}
258
259#[derive(Debug, thiserror::Error)]
260pub enum AlphaVantageError {
261    #[error("API error: {0}")]
262    ApiError(String),
263
264    #[error("Network error: {0}")]
265    Network(#[from] reqwest::Error),
266
267    #[error("Parse error: {0}")]
268    Parse(#[from] serde_json::Error),
269
270    #[error("Rate limit exceeded")]
271    RateLimit,
272
273    #[error(transparent)]
274    Other(#[from] anyhow::Error),
275}