11"""Alpha Vantage API client with rate limiting, caching, and retry logic."""
22
3+ import logging
34import os
45import time
5- import logging
6- from typing import Dict , Any , Optional
76from collections import deque
87from datetime import datetime
8+ from typing import Any , Dict , Optional
99
1010import requests
11- from requests .exceptions import RequestException , Timeout , ConnectionError
11+ from requests .exceptions import ConnectionError , RequestException , Timeout
1212
1313from ..config import (
1414 API_REQUEST_TIMEOUT ,
15- CACHE_TTL_QUOTES ,
1615 CACHE_TTL_HISTORICAL ,
17- RATE_LIMIT_CALLS_PER_MINUTE ,
18- TIMEFRAME_MAP ,
19- ERROR_RATE_LIMIT ,
20- ERROR_NO_DATA ,
16+ CACHE_TTL_QUOTES ,
2117 ERROR_API_KEY_MISSING ,
2218 ERROR_NETWORK ,
19+ ERROR_NO_DATA ,
20+ ERROR_RATE_LIMIT ,
21+ RATE_LIMIT_CALLS_PER_MINUTE ,
22+ TIMEFRAME_MAP ,
2323)
24- from ..utils .formatters import format_error_response , safe_float
2524from ..utils .asset_detector import format_pair_for_alpha_vantage
25+ from ..utils .formatters import format_error_response , safe_float
2626from .cache import ResponseCache
2727
2828logger = logging .getLogger (__name__ )
@@ -36,6 +36,7 @@ def retry_with_exponential_backoff(max_retries: int = 3, base_delay: float = 1.0
3636 max_retries: Maximum number of retry attempts
3737 base_delay: Base delay in seconds (doubles each retry)
3838 """
39+
3940 def decorator (func ):
4041 def wrapper (* args , ** kwargs ):
4142 retries = 0
@@ -45,7 +46,9 @@ def wrapper(*args, **kwargs):
4546 except (Timeout , ConnectionError ) as e :
4647 retries += 1
4748 if retries >= max_retries :
48- logger .error (f"Max retries ({ max_retries } ) reached for { func .__name__ } " )
49+ logger .error (
50+ f"Max retries ({ max_retries } ) reached for { func .__name__ } "
51+ )
4952 raise
5053
5154 delay = base_delay * (2 ** (retries - 1 ))
@@ -59,7 +62,9 @@ def wrapper(*args, **kwargs):
5962 logger .error (f"Request failed with non-retryable error: { e } " )
6063 raise
6164 return func (* args , ** kwargs )
65+
6266 return wrapper
67+
6368 return decorator
6469
6570
@@ -129,8 +134,7 @@ def __init__(self, api_key: Optional[str] = None):
129134 self .base_url = "https://www.alphavantage.co/query"
130135 self .cache = ResponseCache ()
131136 self .rate_limiter = RateLimiter (
132- max_calls = RATE_LIMIT_CALLS_PER_MINUTE ,
133- time_window = 60
137+ max_calls = RATE_LIMIT_CALLS_PER_MINUTE , time_window = 60
134138 )
135139 self ._total_calls = 0
136140
@@ -154,18 +158,18 @@ def _make_request(self, params: Dict[str, str]) -> Dict[str, Any]:
154158 logger .warning (f"Rate limit reached. Need to wait { wait_time :.1f} s" )
155159 return format_error_response (
156160 ERROR_RATE_LIMIT ,
157- suggestion = f"Wait { int (wait_time ) + 1 } seconds before retrying"
161+ suggestion = f"Wait { int (wait_time ) + 1 } seconds before retrying" ,
158162 )
159163
160164 # Add API key to params
161165 params ["apikey" ] = self .api_key
162166
163167 try :
164- logger .debug (f"API request: { params .get ('function' )} for { params .get ('symbol' , 'N/A' )} " )
168+ logger .debug (
169+ f"API request: { params .get ('function' )} for { params .get ('symbol' , 'N/A' )} "
170+ )
165171 response = requests .get (
166- self .base_url ,
167- params = params ,
168- timeout = API_REQUEST_TIMEOUT
172+ self .base_url , params = params , timeout = API_REQUEST_TIMEOUT
169173 )
170174 response .raise_for_status ()
171175
@@ -179,37 +183,27 @@ def _make_request(self, params: Dict[str, str]) -> Dict[str, Any]:
179183 if "Error Message" in data :
180184 logger .error (f"API error: { data ['Error Message' ]} " )
181185 return format_error_response (
182- data ["Error Message" ],
183- suggestion = "Check if the symbol is valid"
186+ data ["Error Message" ], suggestion = "Check if the symbol is valid"
184187 )
185188
186189 if "Note" in data :
187190 logger .warning ("API rate limit message received" )
188- return format_error_response (
189- ERROR_RATE_LIMIT ,
190- details = data ["Note" ]
191- )
191+ return format_error_response (ERROR_RATE_LIMIT , details = data ["Note" ])
192192
193193 return data
194194
195195 except requests .Timeout :
196196 logger .error ("API request timeout" )
197197 return format_error_response (
198198 "Request timeout" ,
199- suggestion = "Try again or check your network connection"
199+ suggestion = "Try again or check your network connection" ,
200200 )
201201 except requests .RequestException as e :
202202 logger .error (f"Network error: { str (e )} " )
203- return format_error_response (
204- ERROR_NETWORK ,
205- details = str (e )
206- )
203+ return format_error_response (ERROR_NETWORK , details = str (e ))
207204 except Exception as e :
208205 logger .error (f"Unexpected error: { str (e )} " )
209- return format_error_response (
210- "Unexpected error occurred" ,
211- details = str (e )
212- )
206+ return format_error_response ("Unexpected error occurred" , details = str (e ))
213207
214208 def get_forex_quote (self , pair : str ) -> Dict [str , Any ]:
215209 """
@@ -248,10 +242,14 @@ def get_forex_quote(self, pair: str) -> Dict[str, Any]:
248242 "symbol" : pair ,
249243 "asset_type" : "forex" ,
250244 "price" : safe_float (rate_data .get ("5. Exchange Rate" , 0 )),
251- "bid" : safe_float (rate_data .get ("8. Bid Price" , rate_data .get ("5. Exchange Rate" , 0 ))),
252- "ask" : safe_float (rate_data .get ("9. Ask Price" , rate_data .get ("5. Exchange Rate" , 0 ))),
245+ "bid" : safe_float (
246+ rate_data .get ("8. Bid Price" , rate_data .get ("5. Exchange Rate" , 0 ))
247+ ),
248+ "ask" : safe_float (
249+ rate_data .get ("9. Ask Price" , rate_data .get ("5. Exchange Rate" , 0 ))
250+ ),
253251 "timestamp" : rate_data .get ("6. Last Refreshed" , "" ),
254- "timezone" : rate_data .get ("7. Time Zone" , "UTC" )
252+ "timezone" : rate_data .get ("7. Time Zone" , "UTC" ),
255253 }
256254 self .cache .set (cache_key , result , CACHE_TTL_QUOTES )
257255 return result
@@ -274,7 +272,7 @@ def get_crypto_quote(self, symbol: str) -> Dict[str, Any]:
274272 return cached
275273
276274 # Remove USD suffix if present
277- crypto_base = symbol .replace (' USD' , '' ) if symbol .endswith (' USD' ) else symbol
275+ crypto_base = symbol .replace (" USD" , "" ) if symbol .endswith (" USD" ) else symbol
278276
279277 params = {
280278 "function" : "CURRENCY_EXCHANGE_RATE" ,
@@ -291,7 +289,10 @@ def get_crypto_quote(self, symbol: str) -> Dict[str, Any]:
291289 rate_data = data ["Realtime Currency Exchange Rate" ]
292290 price = safe_float (rate_data .get ("5. Exchange Rate" , 0 ))
293291
294- from ..config import CRYPTO_SPREAD_MULTIPLIER_BID , CRYPTO_SPREAD_MULTIPLIER_ASK
292+ from ..config import (
293+ CRYPTO_SPREAD_MULTIPLIER_ASK ,
294+ CRYPTO_SPREAD_MULTIPLIER_BID ,
295+ )
295296
296297 result = {
297298 "symbol" : symbol ,
@@ -300,7 +301,7 @@ def get_crypto_quote(self, symbol: str) -> Dict[str, Any]:
300301 "bid" : price * CRYPTO_SPREAD_MULTIPLIER_BID ,
301302 "ask" : price * CRYPTO_SPREAD_MULTIPLIER_ASK ,
302303 "timestamp" : rate_data .get ("6. Last Refreshed" , "" ),
303- "timezone" : rate_data .get ("7. Time Zone" , "UTC" )
304+ "timezone" : rate_data .get ("7. Time Zone" , "UTC" ),
304305 }
305306 self .cache .set (cache_key , result , CACHE_TTL_QUOTES )
306307 return result
@@ -357,10 +358,7 @@ def get_stock_quote(self, symbol: str) -> Dict[str, Any]:
357358 return format_error_response (ERROR_NO_DATA , symbol = symbol )
358359
359360 def get_historical_data_forex (
360- self ,
361- pair : str ,
362- timeframe : str = "1h" ,
363- outputsize : str = "compact"
361+ self , pair : str , timeframe : str = "1h" , outputsize : str = "compact"
364362 ) -> Dict [str , Any ]:
365363 """Get historical forex data."""
366364 cache_key = f"forex_hist_{ pair } _{ timeframe } _{ outputsize } "
@@ -411,10 +409,7 @@ def get_historical_data_forex(
411409 return format_error_response (ERROR_NO_DATA , symbol = pair )
412410
413411 def get_historical_data_stock (
414- self ,
415- symbol : str ,
416- timeframe : str = "1h" ,
417- outputsize : str = "compact"
412+ self , symbol : str , timeframe : str = "1h" , outputsize : str = "compact"
418413 ) -> Dict [str , Any ]:
419414 """
420415 Get historical stock data.
@@ -468,10 +463,7 @@ def get_historical_data_stock(
468463 return format_error_response (ERROR_NO_DATA , symbol = symbol )
469464
470465 def get_historical_data_crypto (
471- self ,
472- symbol : str ,
473- timeframe : str = "1h" ,
474- outputsize : str = "compact"
466+ self , symbol : str , timeframe : str = "1h" , outputsize : str = "compact"
475467 ) -> Dict [str , Any ]:
476468 """
477469 Get historical crypto data.
@@ -490,7 +482,7 @@ def get_historical_data_crypto(
490482 return cached
491483
492484 # Remove USD suffix if present
493- crypto_base = symbol .replace (' USD' , '' ) if symbol .endswith (' USD' ) else symbol
485+ crypto_base = symbol .replace (" USD" , "" ) if symbol .endswith (" USD" ) else symbol
494486
495487 interval = TIMEFRAME_MAP .get (timeframe , "60min" )
496488
@@ -536,6 +528,6 @@ def get_stats(self) -> Dict[str, Any]:
536528 "rate_limit" : {
537529 "max_per_minute" : RATE_LIMIT_CALLS_PER_MINUTE ,
538530 "current_window_calls" : len (self .rate_limiter .calls ),
539- "wait_time_seconds" : self .rate_limiter .wait_time ()
540- }
531+ "wait_time_seconds" : self .rate_limiter .wait_time (),
532+ },
541533 }
0 commit comments