6
6
import os
7
7
import json
8
8
import time
9
- import asyncio
10
- import aiohttp
9
+ import requests
11
10
import logging
12
- import math
13
11
from typing import Dict , List , Optional , Union , Tuple
14
12
from datetime import datetime
15
13
24
22
COINGECKO_API_BASE = "https://api.coingecko.com/api/v3"
25
23
COINGECKO_DEMO_API_KEY = "CG-oWyNSQuvyZMKCDzL3yqGzyrh" # Demo key
26
24
COINGECKO_REQ_PER_MINUTE = 30 # Conservative rate limit
27
- COINGECKO_RATE_LIMIT_DELAY = 60.0 / COINGECKO_REQ_PER_MINUTE
25
+ COINGECKO_RATE_LIMIT_DELAY = 2 # Reduced from 60s to 15s
28
26
MAX_COINS_PER_PAGE = 250 # Maximum allowed by CoinGecko API
29
27
30
28
# --- Global Rate Limiter Variables ---
31
- cg_rate_limit_lock = asyncio .Lock ()
32
29
cg_last_request_time = 0.0
30
+ cg_api_calls = 0 # Counter for API calls
33
31
34
32
# --- Helper Functions ---
35
- async def enforce_rate_limit (last_request_time : float , rate_limit : float ) -> None :
33
+ def increment_api_calls ():
34
+ """Increment the API call counter and log the current count."""
35
+ global cg_api_calls
36
+ cg_api_calls += 1
37
+ logger .info (f"CoinGecko API calls made: { cg_api_calls } " )
38
+
39
+ def enforce_rate_limit (last_request_time : float , rate_limit : float ) -> None :
36
40
"""
37
41
Enforce rate limiting by waiting if necessary.
38
42
@@ -44,9 +48,11 @@ async def enforce_rate_limit(last_request_time: float, rate_limit: float) -> Non
44
48
time_since_last_request = current_time - last_request_time
45
49
46
50
if time_since_last_request < rate_limit :
47
- await asyncio .sleep (rate_limit - time_since_last_request )
51
+ wait_time = rate_limit - time_since_last_request
52
+ logger .info (f"Rate limiting: Waiting { wait_time :.2f} s" )
53
+ time .sleep (wait_time )
48
54
49
- async def make_api_request (
55
+ def make_api_request (
50
56
url : str ,
51
57
method : str = "GET" ,
52
58
headers : Optional [Dict ] = None ,
@@ -68,41 +74,38 @@ async def make_api_request(
68
74
Returns:
69
75
Dict containing the response data or error information
70
76
"""
71
- await enforce_rate_limit (last_request_time , rate_limit )
77
+ enforce_rate_limit (last_request_time , rate_limit )
78
+ increment_api_calls () # Track API call
72
79
73
80
try :
74
- async with aiohttp . ClientSession () as session :
75
- async with session . request (
76
- method = method ,
77
- url = url ,
78
- headers = headers ,
79
- params = params ,
80
- timeout = aiohttp . ClientTimeout ( total = 30 )
81
- ) as response :
82
- if response .status == 429 : # Rate limit exceeded
83
- retry_after = int (response .headers .get ("Retry-After" , "60 " ))
84
- logger .warning (f"Rate limit exceeded. Waiting { retry_after } seconds." )
85
- await asyncio .sleep (retry_after )
86
- return await make_api_request (url , method , headers , params , rate_limit )
87
-
88
- response .raise_for_status ()
89
- return await response .json ()
90
- except aiohttp . ClientError as e :
81
+ response = requests . request (
82
+ method = method ,
83
+ url = url ,
84
+ headers = headers ,
85
+ params = params ,
86
+ timeout = 30
87
+ )
88
+
89
+ if response .status_code == 429 : # Rate limit exceeded
90
+ retry_after = int (response .headers .get ("Retry-After" , "10 " ))
91
+ logger .warning (f"Rate limit exceeded. Waiting { retry_after } seconds." )
92
+ time .sleep (retry_after )
93
+ return make_api_request (url , method , headers , params , rate_limit )
94
+
95
+ response .raise_for_status ()
96
+ return response .json ()
97
+ except requests . RequestException as e :
91
98
logger .error (f"API request failed: { str (e )} " )
92
99
return {"error" : str (e )}
93
- except asyncio .TimeoutError :
94
- logger .error (f"API request timed out: { url } " )
95
- return {"error" : "Request timed out" }
96
100
except Exception as e :
97
101
logger .error (f"Unexpected error during API request: { str (e )} " )
98
102
return {"error" : str (e )}
99
103
100
- async def fetch_coingecko (session : aiohttp . ClientSession , endpoint : str , params : Dict = None ) -> Optional [Union [List , Dict ]]:
104
+ def fetch_coingecko (endpoint : str , params : Dict = None ) -> Optional [Union [List , Dict ]]:
101
105
"""
102
106
Generic CoinGecko fetcher that exactly matches the successful example.
103
107
104
108
Args:
105
- session: aiohttp client session
106
109
endpoint: API endpoint to fetch
107
110
params: Optional query parameters
108
111
@@ -127,57 +130,56 @@ async def fetch_coingecko(session: aiohttp.ClientSession, endpoint: str, params:
127
130
128
131
for retry in range (max_retries ):
129
132
# Apply rate limiting
130
- async with cg_rate_limit_lock :
131
- wait_time = COINGECKO_RATE_LIMIT_DELAY - (time .time () - cg_last_request_time )
132
- if wait_time > 0 :
133
- logger .debug (f"CG Rate limiting. Wait: { wait_time :.2f} s" )
134
- await asyncio .sleep (wait_time )
135
- cg_last_request_time = time .time ()
133
+ wait_time = COINGECKO_RATE_LIMIT_DELAY - (time .time () - cg_last_request_time )
134
+ if wait_time > 0 :
135
+ logger .debug (f"CG Rate limiting. Wait: { wait_time :.2f} s" )
136
+ time .sleep (wait_time )
137
+ cg_last_request_time = time .time ()
136
138
137
139
try :
138
140
logger .info (f"Fetching CG URL: { url } with params: { params } " )
139
141
logger .info (f"Headers: { headers } " )
140
142
141
143
# Make the request exactly as in the example
142
- async with session .get (url , headers = headers , params = params , timeout = 20 ) as response :
143
- response_text = await response .text ()
144
-
145
- if response .status == 200 :
146
- try :
147
- data = json .loads (response_text )
148
- logger .info (f"Successfully received data from CoinGecko API" )
149
- return data
150
- except json .JSONDecodeError :
151
- logger .error (f"Failed to parse JSON response: { response_text [:100 ]} ..." )
152
- return None
153
- else :
154
- logger .error (f"CG request failed { url } : Status { response .status } , Response: { response_text [:100 ]} ..." )
155
-
156
- if response .status == 429 :
157
- # Rate limit hit
158
- logger .warning (f"CG rate limit hit for { url } . Retrying after delay." )
159
- retry_delay *= 2 # Exponential backoff
160
- await asyncio .sleep (retry_delay )
161
- continue
162
- elif retry < max_retries - 1 :
163
- retry_delay *= 1.5
164
- await asyncio .sleep (retry_delay )
165
- continue
166
-
144
+ response = requests .get (url , headers = headers , params = params , timeout = 20 )
145
+ response_text = response .text
146
+
147
+ if response .status_code == 200 :
148
+ try :
149
+ data = json .loads (response_text )
150
+ logger .info (f"Successfully received data from CoinGecko API" )
151
+ return data
152
+ except json .JSONDecodeError :
153
+ logger .error (f"Failed to parse JSON response: { response_text [:100 ]} ..." )
167
154
return None
155
+ else :
156
+ logger .error (f"CG request failed { url } : Status { response .status_code } , Response: { response_text [:100 ]} ..." )
157
+
158
+ if response .status_code == 429 :
159
+ # Rate limit hit
160
+ logger .warning (f"CG rate limit hit for { url } . Retrying after delay." )
161
+ retry_delay *= 2 # Exponential backoff
162
+ time .sleep (retry_delay )
163
+ continue
164
+ elif retry < max_retries - 1 :
165
+ retry_delay *= 1.5
166
+ time .sleep (retry_delay )
167
+ continue
168
+
169
+ return None
168
170
169
- except asyncio . TimeoutError :
171
+ except requests . Timeout :
170
172
logger .warning (f"CG request timeout for { url } " )
171
173
if retry < max_retries - 1 :
172
174
retry_delay *= 1.5
173
- await asyncio .sleep (retry_delay )
175
+ time .sleep (retry_delay )
174
176
continue
175
177
return None
176
178
except Exception as e :
177
179
logger .error (f"Error fetching CG { url } : { e } " )
178
180
if retry < max_retries - 1 :
179
181
retry_delay *= 1.5
180
- await asyncio .sleep (retry_delay )
182
+ time .sleep (retry_delay )
181
183
continue
182
184
return None
183
185
@@ -213,12 +215,11 @@ def calculate_pagination(total_tokens: int) -> List[Tuple[int, int]]:
213
215
214
216
return pagination
215
217
216
- async def fetch_all_coingecko_market_data (session : aiohttp . ClientSession , number_of_tokens : int = 250 ) -> dict :
218
+ def fetch_all_coingecko_market_data (number_of_tokens : int = 250 ) -> dict :
217
219
"""
218
220
Fetches data from CG /coins/markets using the exact parameters that worked.
219
221
220
222
Args:
221
- session: aiohttp client session
222
223
number_of_tokens: Number of top tokens by market cap to fetch (default: 250)
223
224
224
225
Returns:
@@ -251,7 +252,7 @@ async def fetch_all_coingecko_market_data(session: aiohttp.ClientSession, number
251
252
252
253
# Make API request
253
254
endpoint = '/coins/markets'
254
- data = await fetch_coingecko (session , endpoint , params )
255
+ data = fetch_coingecko (endpoint , params )
255
256
256
257
if not data or not isinstance (data , list ):
257
258
logger .warning (f"No valid data received for page { page_num } . Skipping." )
@@ -303,7 +304,7 @@ async def fetch_all_coingecko_market_data(session: aiohttp.ClientSession, number
303
304
304
305
# Respect rate limiting between page requests
305
306
if page_num < total_pages :
306
- await asyncio .sleep (COINGECKO_RATE_LIMIT_DELAY )
307
+ time .sleep (COINGECKO_RATE_LIMIT_DELAY )
307
308
308
309
if total_tokens_received < number_of_tokens :
309
310
logger .warning (f"Requested { number_of_tokens } tokens but only received { total_tokens_received } . This appears to be all available tokens." )
@@ -314,12 +315,11 @@ async def fetch_all_coingecko_market_data(session: aiohttp.ClientSession, number
314
315
logger .error (f"Error fetching CoinGecko market data: { e } " , exc_info = True )
315
316
return all_markets_data
316
317
317
- async def fetch_coin_volume (session : aiohttp . ClientSession , coin_id : str ) -> Optional [float ]:
318
+ def fetch_coin_volume (coin_id : str ) -> Optional [float ]:
318
319
"""
319
320
Helper to fetch 30d volume data for a single coin.
320
321
321
322
Args:
322
- session: aiohttp client session
323
323
coin_id: CoinGecko coin ID
324
324
325
325
Returns:
@@ -335,7 +335,7 @@ async def fetch_coin_volume(session: aiohttp.ClientSession, coin_id: str) -> Opt
335
335
}
336
336
337
337
endpoint = f"/coins/{ coin_id } /market_chart"
338
- data = await fetch_coingecko (session , endpoint , params )
338
+ data = fetch_coingecko (endpoint , params )
339
339
340
340
if not data or 'total_volumes' not in data or not data ['total_volumes' ]:
341
341
logger .warning (f"No volume data received for { coin_id } " )
@@ -356,14 +356,13 @@ async def fetch_coin_volume(session: aiohttp.ClientSession, coin_id: str) -> Opt
356
356
357
357
except Exception as e :
358
358
logger .error (f"Error fetching volume data for { coin_id } : { e } " )
359
- raise # Re-raise to be caught by gather
359
+ return None
360
360
361
- async def fetch_all_coingecko_volumes (session : aiohttp . ClientSession , coin_ids : list ) -> dict :
361
+ def fetch_all_coingecko_volumes (coin_ids : list ) -> dict :
362
362
"""
363
363
Fetches 30d volume data from CG /coins/{id}/market_chart.
364
364
365
365
Args:
366
- session: aiohttp client session
367
366
coin_ids: List of CoinGecko coin IDs
368
367
369
368
Returns:
@@ -375,13 +374,13 @@ async def fetch_all_coingecko_volumes(session: aiohttp.ClientSession, coin_ids:
375
374
# Process coins one by one to avoid rate limiting issues
376
375
for coin_id in coin_ids :
377
376
try :
378
- total_volume = await fetch_coin_volume (session , coin_id )
377
+ total_volume = fetch_coin_volume (coin_id )
379
378
if total_volume is not None :
380
379
volume_data [coin_id ] = total_volume
381
380
logger .info (f"Processed 30d volume for { coin_id } : ${ total_volume :,.2f} " )
382
381
383
382
# Respect rate limiting between requests
384
- await asyncio .sleep (COINGECKO_RATE_LIMIT_DELAY )
383
+ time .sleep (COINGECKO_RATE_LIMIT_DELAY )
385
384
386
385
except Exception as e :
387
386
logger .warning (f"Error processing volume for { coin_id } : { str (e )} " )
@@ -390,7 +389,7 @@ async def fetch_all_coingecko_volumes(session: aiohttp.ClientSession, coin_ids:
390
389
logger .info (f"Successfully fetched 30d volume data for { len (volume_data )} /{ len (coin_ids )} coins" )
391
390
return volume_data
392
391
393
- async def fetch_coingecko_data (symbol : str ) -> Dict :
392
+ def fetch_coingecko_data (symbol : str ) -> Dict :
394
393
"""
395
394
Fetch market data from CoinGecko API for a given symbol.
396
395
@@ -404,7 +403,7 @@ async def fetch_coingecko_data(symbol: str) -> Dict:
404
403
405
404
# First, get the coin ID
406
405
search_url = f"{ COINGECKO_API_BASE } /search"
407
- search_response = await make_api_request (
406
+ search_response = make_api_request (
408
407
url = search_url ,
409
408
params = {"query" : symbol },
410
409
rate_limit = COINGECKO_RATE_LIMIT_DELAY ,
@@ -431,7 +430,7 @@ async def fetch_coingecko_data(symbol: str) -> Dict:
431
430
432
431
# Fetch detailed market data
433
432
market_url = f"{ COINGECKO_API_BASE } /coins/{ coin_id } "
434
- market_response = await make_api_request (
433
+ market_response = make_api_request (
435
434
url = market_url ,
436
435
params = {
437
436
"localization" : "false" ,
@@ -468,23 +467,23 @@ async def fetch_coingecko_data(symbol: str) -> Dict:
468
467
"coingecko_ath_change_percentage" : market_data .get ("ath_change_percentage" , {}).get ("usd" )
469
468
}
470
469
471
- async def fetch_coingecko_historical_volume (coin_id : str , days : int = 30 ) -> float :
470
+ def fetch_coingecko_historical_volume (coin_id : str , number_of_days : int = 30 ) -> float :
472
471
"""
473
472
Fetch historical volume data from CoinGecko API.
474
473
475
474
Args:
476
475
coin_id: The CoinGecko coin ID
477
- days : Number of days of historical data to fetch
476
+ number_of_days : Number of days of historical data to fetch (default: 30)
478
477
479
478
Returns:
480
479
Total volume over the specified period or 0 if error
481
480
"""
482
481
global cg_last_request_time
483
482
484
483
url = f"{ COINGECKO_API_BASE } /coins/{ coin_id } /market_chart"
485
- response = await make_api_request (
484
+ response = make_api_request (
486
485
url = url ,
487
- params = {"vs_currency" : "usd" , "days" : str (days ), "interval" : "daily" },
486
+ params = {"vs_currency" : "usd" , "days" : str (number_of_days ), "interval" : "daily" },
488
487
rate_limit = COINGECKO_RATE_LIMIT_DELAY ,
489
488
last_request_time = cg_last_request_time
490
489
)
0 commit comments