borsa_alphavantage/
lib.rs1#![warn(missing_docs)]
8use std::sync::Arc;
9
10use async_trait::async_trait;
11
12use borsa_core::{
13 AssetKind, BorsaError, HistoryRequest, HistoryResponse, Instrument, Quote, SearchRequest,
14 SearchResponse,
15 connector::{
16 BorsaConnector, ConnectorKey, HistoryProvider, QuoteProvider,
17 SearchProvider,
18 },
19};
20
21pub mod adapter;
23mod convert;
24
25#[cfg(feature = "test-adapters")]
26use adapter::CloneArcAdapters;
27use adapter::{AvHistory, AvQuotes, AvSearch, RealAdapter};
28
29#[cfg(not(feature = "test-adapters"))]
30type AdapterArc = Arc<RealAdapter>;
31
32#[cfg(feature = "test-adapters")]
33type QuotesAdapter = Arc<dyn AvQuotes>;
34#[cfg(not(feature = "test-adapters"))]
35type QuotesAdapter = AdapterArc;
36
37#[cfg(feature = "test-adapters")]
38type HistoryAdapter = Arc<dyn AvHistory>;
39#[cfg(not(feature = "test-adapters"))]
40type HistoryAdapter = AdapterArc;
41
42#[cfg(feature = "test-adapters")]
43type SearchAdapter = Arc<dyn AvSearch>;
44#[cfg(not(feature = "test-adapters"))]
45type SearchAdapter = AdapterArc;
46
47pub struct AvConnector {
56 quotes: QuotesAdapter,
57 history: HistoryAdapter,
58 search: SearchAdapter,
59 }
61
62impl AvConnector {
63 pub fn new_with_key(key: impl Into<String>) -> Self {
65 let a = RealAdapter::new_with_key(key);
66 Self::from_adapter(&a)
67 }
68
69 pub fn new_with_rapidapi(key: impl Into<String>) -> Self {
71 let a = RealAdapter::new_with_rapidapi(key);
72 Self::from_adapter(&a)
73 }
74
75 pub fn new_with_key_and_client(key: impl Into<String>, http: reqwest::Client) -> Self {
77 let a = RealAdapter::new_with_key_and_client(key, http);
78 Self::from_adapter(&a)
79 }
80
81 pub fn new_with_rapidapi_and_client(key: impl Into<String>, http: reqwest::Client) -> Self {
83 let a = RealAdapter::new_with_rapidapi_and_client(key, http);
84 Self::from_adapter(&a)
85 }
86
87 #[cfg(feature = "test-adapters")]
89 #[must_use]
90 pub fn from_adapter<A: CloneArcAdapters + 'static>(adapter: &A) -> Self {
91 Self {
92 quotes: adapter.clone_arc_quotes(),
93 history: adapter.clone_arc_history(),
94 search: adapter.clone_arc_search(),
95 }
97 }
98
99 #[cfg(not(feature = "test-adapters"))]
101 #[must_use]
102 pub fn from_adapter(adapter: &RealAdapter) -> Self {
103 let shared = Arc::new(adapter.clone());
104 Self {
105 quotes: Arc::clone(&shared),
106 history: Arc::clone(&shared),
107 search: Arc::clone(&shared),
108 }
110 }
111
112 pub const KEY: ConnectorKey = ConnectorKey::new("borsa-alphavantage");
114
115 fn looks_like_not_found(msg: &str) -> bool {
116 let m = msg.to_ascii_lowercase();
117 m.contains("invalid api call")
118 || m.contains("no data")
119 || m.contains("not found")
120 || m.contains("unknown symbol")
121 || m.contains("no matches")
122 }
123
124 fn normalize_error(e: BorsaError, what: &str) -> BorsaError {
125 match e {
126 BorsaError::Connector { connector, msg } => {
127 if Self::looks_like_not_found(&msg) {
128 BorsaError::not_found(what.to_string())
129 } else {
130 BorsaError::Connector { connector, msg }
131 }
132 }
133 BorsaError::Other(msg) => {
134 if Self::looks_like_not_found(&msg) {
135 BorsaError::not_found(what.to_string())
136 } else {
137 BorsaError::connector("borsa-alphavantage", msg)
138 }
139 }
140 other => other,
141 }
142 }
143
144 fn parse_forex_pair(symbol: &str) -> Result<(&str, &str), BorsaError> {
147 if let Some(pos) = symbol.find('/') {
149 let base = &symbol[..pos];
150 let quote = &symbol[pos + 1..];
151 if base.is_empty() || quote.is_empty() {
152 return Err(BorsaError::InvalidArg(format!(
153 "Invalid forex pair format: '{symbol}' - empty base or quote currency"
154 )));
155 }
156 return Ok((base, quote));
157 }
158
159 if let Some(pos) = symbol.find('-') {
160 let base = &symbol[..pos];
161 let quote = &symbol[pos + 1..];
162 if base.is_empty() || quote.is_empty() {
163 return Err(BorsaError::InvalidArg(format!(
164 "Invalid forex pair format: '{symbol}' - empty base or quote currency"
165 )));
166 }
167 return Ok((base, quote));
168 }
169
170 Err(BorsaError::InvalidArg(format!(
172 "Forex pair for AlphaVantage must be in 'BASE/QUOTE' format, got: '{symbol}'"
173 )))
174 }
175}
176
177#[async_trait]
178impl QuoteProvider for AvConnector {
179 async fn quote(&self, instrument: &Instrument) -> Result<Quote, BorsaError> {
180 self.quotes
181 .quote_equity(instrument.symbol_str())
182 .await
183 .map_err(|e| Self::normalize_error(e, &format!("quote for {}", instrument.symbol())))
184 }
185}
186
187#[async_trait]
188impl HistoryProvider for AvConnector {
189 async fn history(
190 &self,
191 instrument: &Instrument,
192 req: HistoryRequest,
193 ) -> Result<HistoryResponse, BorsaError> {
194 match instrument.kind() {
195 AssetKind::Forex => {
196 let (base, quote) = Self::parse_forex_pair(instrument.symbol_str())?;
197 self.history.forex(base, quote, &req).await.map_err(|e| {
198 Self::normalize_error(e, &format!("history for {}", instrument.symbol()))
199 })
200 }
201 AssetKind::Crypto => self
202 .history
203 .crypto(instrument.symbol_str(), &req)
204 .await
205 .map_err(|e| {
206 Self::normalize_error(e, &format!("history for {}", instrument.symbol()))
207 }),
208 _ => self
209 .history
210 .equity(instrument.symbol_str(), &req)
211 .await
212 .map_err(|e| {
213 Self::normalize_error(e, &format!("history for {}", instrument.symbol()))
214 }),
215 }
216 }
217
218 fn supported_history_intervals(
219 &self,
220 _kind: AssetKind,
221 ) -> &'static [borsa_core::types::Interval] {
222 use borsa_core::types::Interval as I;
223 const AV_INTERVALS: &[I] = &[
224 I::I1m,
225 I::I5m,
226 I::I15m,
227 I::I30m,
228 I::I1h,
229 I::D1,
230 I::W1,
231 I::M1,
232 ];
233 AV_INTERVALS
234 }
235}
236
237#[async_trait]
238impl SearchProvider for AvConnector {
239 async fn search(&self, req: SearchRequest) -> Result<SearchResponse, BorsaError> {
240 let mut results = self
241 .search
242 .search(&req)
243 .await
244 .map_err(|e| Self::normalize_error(e, "search"))?;
245 if let Some(limit) = req.limit() {
246 results.truncate(limit);
247 }
248 Ok(SearchResponse { results })
249 }
250}
251
252#[async_trait]
265impl BorsaConnector for AvConnector {
266 fn name(&self) -> &'static str {
267 "borsa-alphavantage"
268 }
269
270 fn vendor(&self) -> &'static str {
271 "Alpha Vantage"
272 }
273
274 fn as_history_provider(&self) -> Option<&dyn borsa_core::connector::HistoryProvider> {
277 Some(self as &dyn HistoryProvider)
278 }
279 fn as_quote_provider(&self) -> Option<&dyn borsa_core::connector::QuoteProvider> {
280 Some(self as &dyn QuoteProvider)
281 }
282 fn as_search_provider(&self) -> Option<&dyn borsa_core::connector::SearchProvider> {
283 Some(self as &dyn SearchProvider)
284 }
285 fn supports_kind(&self, kind: AssetKind) -> bool {
293 matches!(
294 kind,
295 AssetKind::Equity | AssetKind::Forex | AssetKind::Crypto
296 )
297 }
298}