Scraping Crypto Prices with Python: CoinGecko & Binance APIs (2026)
Scraping Crypto Prices with Python: CoinGecko & Binance APIs (2026)
If you're building a trading bot, a portfolio tracker, or just doing market research, you need reliable price data. The good news is two of the best crypto data sources — CoinGecko and Binance — offer free REST APIs that don't require authentication for basic usage. No API key signup, no OAuth dance. Just HTTP GET requests and JSON.
This guide covers both APIs in depth: what data they expose, how to handle pagination and rate limits, how to store data locally, and when you need proxy rotation for high-frequency collection.
Why Two APIs?
CoinGecko and Binance serve different use cases:
CoinGecko is better for: - Market cap rankings and coin metadata - Multi-currency price conversions (USD, EUR, BTC, ETH, etc.) - Historical data for coins not on major exchanges - Coin descriptions, links, and social stats - Covering thousands of altcoins, not just top-tier pairs
Binance is better for: - High-frequency OHLCV data with volume - Real exchange data (not aggregated) - Sub-hourly intervals (1m, 5m, 15m) - Futures and perpetual contract data - Precise trade-level data for analysis
For most projects, you'll use both.
CoinGecko Free API: Top Coins by Market Cap
CoinGecko's /coins/markets endpoint gives you price, market cap, volume, and 24h change for any list of coins. The free tier allows roughly 10-30 calls per minute without a key.
import requests
import time
def get_top_coins(vs_currency="usd", per_page=100, page=1):
"""Get coins ranked by market cap."""
url = "https://api.coingecko.com/api/v3/coins/markets"
params = {
"vs_currency": vs_currency,
"order": "market_cap_desc",
"per_page": per_page,
"page": page,
"sparkline": False,
"price_change_percentage": "1h,24h,7d"
}
response = requests.get(url, params=params, timeout=15)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
return get_top_coins(vs_currency, per_page, page)
response.raise_for_status()
return response.json()
coins = get_top_coins(per_page=50)
for coin in coins[:10]:
change_1h = coin.get("price_change_percentage_1h_in_currency", 0) or 0
change_24h = coin.get("price_change_percentage_24h", 0) or 0
print(f"{coin['symbol'].upper():8} ${coin['current_price']:>12,.4f} "
f"1h: {change_1h:+.2f}% 24h: {change_24h:+.2f}% "
f"MCap: ${coin['market_cap']:>15,.0f}")
This gives you a clean table of the top coins sorted by market cap. The order param also accepts volume_desc, id_asc, and a few others.
CoinGecko: Get All Coin IDs
Before you can fetch data for a specific coin, you need its CoinGecko ID (which is different from its ticker symbol):
def get_all_coins():
"""Get the complete list of coins with their CoinGecko IDs."""
url = "https://api.coingecko.com/api/v3/coins/list"
response = requests.get(url, timeout=30)
response.raise_for_status()
coins = response.json()
# Returns list of {id, symbol, name}
# e.g., {"id": "bitcoin", "symbol": "btc", "name": "Bitcoin"}
return {coin["symbol"].lower(): coin["id"] for coin in coins}
symbol_to_id = get_all_coins()
print(f"Total coins: {len(symbol_to_id)}")
print(f"ETH id: {symbol_to_id.get('eth')}") # "ethereum"
print(f"SOL id: {symbol_to_id.get('sol')}") # "solana"
Note: some symbols map to multiple coins (e.g., many tokens called "DOGE"). The list returns all of them — you'll need to pick the right one.
CoinGecko OHLCV Data
For candlestick data, use /coins/{id}/ohlc. Returns OHLCV candles for 1, 7, 14, 30, 90, 180, or 365 day windows. The granularity is automatic -- under 2 days you get hourly candles, longer ranges give you daily.
def get_ohlcv(coin_id="bitcoin", vs_currency="usd", days=30):
"""Get OHLCV candlestick data from CoinGecko."""
url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/ohlc"
params = {
"vs_currency": vs_currency,
"days": days
}
response = requests.get(url, params=params, timeout=15)
if response.status_code == 429:
time.sleep(60)
return get_ohlcv(coin_id, vs_currency, days)
response.raise_for_status()
data = response.json()
# Each entry: [timestamp_ms, open, high, low, close]
candles = []
for entry in data:
candles.append({
"timestamp": entry[0] / 1000, # convert ms to seconds
"open": entry[1],
"high": entry[2],
"low": entry[3],
"close": entry[4]
})
return candles
btc_candles = get_ohlcv("bitcoin", days=30)
print(f"Got {len(btc_candles)} candles")
last = btc_candles[-1]
print(f"Latest: O={last['open']:.2f} H={last['high']:.2f} L={last['low']:.2f} C={last['close']:.2f}")
Note: CoinGecko doesn't include volume in the OHLCV response on the free tier. If you need volume, pull it separately from /coins/{id}/market_chart or switch to Binance.
CoinGecko: Historical Price Chart
The market_chart endpoint gives you more granular control over the time range:
from datetime import datetime, timedelta
import time as time_module
def get_price_history(coin_id, days=365, vs_currency="usd"):
"""Get full price + volume + market cap history."""
url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/market_chart"
params = {
"vs_currency": vs_currency,
"days": days,
"interval": "daily" # 'daily' or leave empty for auto
}
response = requests.get(url, params=params, timeout=20)
if response.status_code == 429:
time_module.sleep(60)
return get_price_history(coin_id, days, vs_currency)
response.raise_for_status()
data = response.json()
# Convert parallel arrays to records
prices = data["prices"] # [[timestamp_ms, price], ...]
volumes = data["total_volumes"]
market_caps = data["market_caps"]
records = []
for i in range(len(prices)):
records.append({
"timestamp": prices[i][0] / 1000,
"price": prices[i][1],
"volume": volumes[i][1] if i < len(volumes) else None,
"market_cap": market_caps[i][1] if i < len(market_caps) else None,
})
return records
history = get_price_history("ethereum", days=365)
print(f"Got {len(history)} daily data points")
print(f"Price range: ${min(r['price'] for r in history):,.2f} - ${max(r['price'] for r in history):,.2f}")
Binance REST API: Historical Klines
Binance is the better source when you need high-resolution historical data or volume. Their /api/v3/klines endpoint returns full OHLCV candles with no authentication required for market data.
def get_binance_klines(symbol="BTCUSDT", interval="1d", limit=500, start_time=None, end_time=None):
"""Get OHLCV kline data from Binance."""
url = "https://api.binance.com/api/v3/klines"
params = {
"symbol": symbol,
"interval": interval, # 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
"limit": min(limit, 1000) # max 1000 per request
}
if start_time:
params["startTime"] = int(start_time * 1000) # seconds to ms
if end_time:
params["endTime"] = int(end_time * 1000)
response = requests.get(url, params=params, timeout=15)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 30))
time.sleep(retry_after)
return get_binance_klines(symbol, interval, limit, start_time, end_time)
response.raise_for_status()
raw = response.json()
candles = []
for k in raw:
candles.append({
"open_time": k[0] / 1000,
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5]), # base asset volume (BTC for BTCUSDT)
"close_time": k[6] / 1000,
"quote_volume": float(k[7]), # quote asset volume (USDT for BTCUSDT)
"trades": int(k[8]), # number of trades
"taker_buy_volume": float(k[9]),
"taker_buy_quote_volume": float(k[10]),
})
return candles
klines = get_binance_klines("BTCUSDT", "1d", 365)
last = klines[-1]
print(f"Last close: ${last['close']:,.2f}, Volume: {last['volume']:.2f} BTC ({last['trades']:,} trades)")
Binance: Paginating Historical Data Beyond 1000 Candles
To get more than 1000 candles, paginate using startTime and endTime:
from datetime import datetime, timedelta
import time
def get_full_history_binance(symbol, interval, start_date, end_date=None):
"""Get complete OHLCV history for a symbol between dates."""
if end_date is None:
end_date = datetime.now()
# Convert interval to seconds for pagination
interval_seconds = {
"1m": 60, "5m": 300, "15m": 900, "30m": 1800,
"1h": 3600, "4h": 14400, "1d": 86400, "1w": 604800
}.get(interval, 86400)
all_candles = []
current_start = start_date.timestamp()
end_ts = end_date.timestamp()
while current_start < end_ts:
# Each request gets at most 1000 candles
batch_end = min(current_start + interval_seconds * 1000, end_ts)
batch = get_binance_klines(
symbol, interval, limit=1000,
start_time=current_start,
end_time=batch_end
)
if not batch:
break
all_candles.extend(batch)
current_start = batch[-1]["close_time"] + 1 # start after last candle
print(f" Fetched {len(all_candles)} candles, up to {datetime.fromtimestamp(batch[-1]['open_time']).date()}")
time.sleep(0.2) # stay under weight limits
return all_candles
# Get 2 years of hourly data for ETH
start = datetime(2024, 1, 1)
candles = get_full_history_binance("ETHUSDT", "1h", start)
print(f"Total hourly candles: {len(candles)}")
Binance 24hr Ticker
For a quick snapshot of current price, volume, and 24h change across all pairs:
def get_binance_ticker(symbol=None):
"""Get 24hr price statistics. Omit symbol for all pairs."""
url = "https://api.binance.com/api/v3/ticker/24hr"
params = {}
if symbol:
params["symbol"] = symbol
response = requests.get(url, params=params, timeout=15)
response.raise_for_status()
return response.json()
# Single symbol
btc = get_binance_ticker("BTCUSDT")
print(f"BTC Price: ${float(btc['lastPrice']):,.2f}")
print(f"24h change: {float(btc['priceChangePercent']):+.2f}%")
print(f"24h high: ${float(btc['highPrice']):,.2f}")
print(f"24h low: ${float(btc['lowPrice']):,.2f}")
print(f"24h volume: ${float(btc['quoteVolume']):,.0f}")
# All tickers (returns a list of ~2000+ pairs)
all_tickers = get_binance_ticker()
usdt_pairs = [t for t in all_tickers if t["symbol"].endswith("USDT")]
# Sort by 24h volume
usdt_pairs.sort(key=lambda x: float(x["quoteVolume"]), reverse=True)
print(f"\nTop 5 USDT pairs by volume:")
for t in usdt_pairs[:5]:
print(f" {t['symbol']:12} ${float(t['lastPrice']):>12,.4f} {float(t['priceChangePercent']):+.2f}%")
Calling without a symbol returns data for all trading pairs at once. One request instead of thousands -- use this for broad market scans.
Rate Limits and Backoff
CoinGecko free tier: ~10-30 requests/minute (no key), ~50/minute (with free key from coingecko.com/en/api). The Retry-After header tells you exactly how long to wait on a 429.
Binance uses a weight system. Most endpoints cost 1-10 weight units, and you get 1200 per minute. The X-MBX-USED-WEIGHT-1M response header shows your current consumption. The /klines endpoint costs 1 weight unit per call.
import time
class RateLimitedRequester:
"""Simple rate-limited HTTP client with exponential backoff."""
def __init__(self, requests_per_minute=20):
self.min_interval = 60.0 / requests_per_minute
self.last_request = 0
def get(self, url, params=None, max_retries=5, **kwargs):
for attempt in range(max_retries):
# Enforce minimum interval
elapsed = time.time() - self.last_request
if elapsed < self.min_interval:
time.sleep(self.min_interval - elapsed)
try:
response = requests.get(url, params=params, timeout=15, **kwargs)
self.last_request = time.time()
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 2 ** attempt))
print(f"Rate limited. Waiting {wait}s (attempt {attempt+1}/{max_retries})")
time.sleep(wait)
continue
response.raise_for_status()
return response
except requests.exceptions.Timeout:
wait = 2 ** attempt
print(f"Timeout on attempt {attempt+1}, retrying in {wait}s")
time.sleep(wait)
raise Exception(f"Failed after {max_retries} attempts: {url}")
# Usage
cg_client = RateLimitedRequester(requests_per_minute=20)
response = cg_client.get(
"https://api.coingecko.com/api/v3/coins/markets",
params={"vs_currency": "usd", "per_page": 100}
)
Proxies for High-Frequency Collection
The free tier limits work fine for personal trackers and occasional scripts. But if you're monitoring hundreds of trading pairs continuously — running market scanners, building live dashboards, or doing competitive analysis — you'll hit rate limits fast, even with backoff.
When monitoring at scale, proxy requests through ThorData to distribute load across different IPs, staying under per-IP rate limits without getting blocked:
proxies = {
"http": "http://user:[email protected]:PORT",
"https": "http://user:[email protected]:PORT"
}
response = requests.get(
"https://api.coingecko.com/api/v3/coins/markets",
params={"vs_currency": "usd", "per_page": 100},
proxies=proxies,
timeout=20
)
You can get rotating residential proxies through ThorData's affiliate program. Worth it once you're past the hobby stage and need reliable uptime.
Saving Data to SQLite
Once you're pulling data, store it locally. SQLite is the right call for local storage:
import sqlite3
from datetime import datetime
def init_db(db_path="crypto.db"):
conn = sqlite3.connect(db_path)
conn.executescript("""
CREATE TABLE IF NOT EXISTS candles (
symbol TEXT,
interval TEXT,
open_time INTEGER,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
quote_volume REAL,
trades INTEGER,
source TEXT DEFAULT 'binance',
PRIMARY KEY (symbol, interval, open_time)
);
CREATE TABLE IF NOT EXISTS prices (
coin_id TEXT,
timestamp INTEGER,
price REAL,
market_cap REAL,
volume_24h REAL,
source TEXT DEFAULT 'coingecko',
PRIMARY KEY (coin_id, timestamp)
);
CREATE INDEX IF NOT EXISTS idx_candles_symbol ON candles(symbol, interval);
CREATE INDEX IF NOT EXISTS idx_prices_coin ON prices(coin_id);
""")
conn.commit()
return conn
def save_candles(conn, candles, symbol, interval="1d"):
rows = [
(symbol, interval, int(c["open_time"]), c["open"], c["high"],
c["low"], c["close"], c["volume"], c.get("quote_volume", 0), c.get("trades", 0))
for c in candles
]
conn.executemany(
"""INSERT OR REPLACE INTO candles
(symbol, interval, open_time, open, high, low, close, volume, quote_volume, trades)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
rows
)
conn.commit()
print(f"Saved {len(rows)} candles for {symbol}/{interval}")
conn = init_db()
klines = get_binance_klines("BTCUSDT", "1d", 365)
save_candles(conn, klines, "BTCUSDT", "1d")
conn.close()
INSERT OR REPLACE handles deduplication automatically -- run the same fetch twice and you won't get duplicate rows.
Building a Simple Price Alert System
import sqlite3
import time
from datetime import datetime
def check_price_alerts(symbols, thresholds, db_path="crypto.db"):
"""
Check prices against thresholds and alert on crosses.
thresholds: {"BTCUSDT": {"above": 100000, "below": 80000}}
"""
conn = sqlite3.connect(db_path)
for symbol in symbols:
ticker = get_binance_ticker(symbol)
current_price = float(ticker["lastPrice"])
change_24h = float(ticker["priceChangePercent"])
limits = thresholds.get(symbol, {})
if "above" in limits and current_price > limits["above"]:
print(f"ALERT: {symbol} crossed ABOVE ${limits['above']:,.2f} "
f"(current: ${current_price:,.2f}, 24h: {change_24h:+.2f}%)")
if "below" in limits and current_price < limits["below"]:
print(f"ALERT: {symbol} crossed BELOW ${limits['below']:,.2f} "
f"(current: ${current_price:,.2f}, 24h: {change_24h:+.2f}%)")
# Store for history
conn.execute(
"INSERT OR REPLACE INTO prices (coin_id, timestamp, price) VALUES (?,?,?)",
(symbol, int(time.time()), current_price)
)
conn.commit()
conn.close()
# Run every 5 minutes
thresholds = {
"BTCUSDT": {"above": 120000, "below": 85000},
"ETHUSDT": {"above": 5000, "below": 2500},
}
while True:
check_price_alerts(["BTCUSDT", "ETHUSDT", "SOLUSDT"], thresholds)
time.sleep(300) # 5 minutes
Multi-Exchange Comparison
Cross-exchange price differences (arbitrage spreads) are useful for trading analysis. Add Kraken and Coinbase alongside Binance:
def get_kraken_ticker(pair: str = "XBTUSD") -> dict:
"""Fetch current price from Kraken's public API."""
url = "https://api.kraken.com/0/public/Ticker"
resp = requests.get(url, params={"pair": pair}, timeout=15)
resp.raise_for_status()
data = resp.json()
result = data.get("result", {})
if result:
ticker = list(result.values())[0]
return {
"pair": pair,
"last": float(ticker["c"][0]),
"bid": float(ticker["b"][0]),
"ask": float(ticker["a"][0]),
"volume_24h": float(ticker["v"][1]),
}
return {}
def get_coinbase_price(product_id: str = "BTC-USD") -> dict:
"""Fetch current price from Coinbase Advanced Trade API."""
url = f"https://api.coinbase.com/api/v3/brokerage/market/products/{product_id}"
resp = requests.get(url, timeout=15)
resp.raise_for_status()
data = resp.json()
return {
"product_id": product_id,
"price": float(data.get("price", 0)),
"volume_24h": float(data.get("volume_24h", 0)),
}
def compare_exchange_prices(coin: str = "BTC") -> dict:
"""Compare prices for a coin across Binance, Kraken, and Coinbase."""
results = {}
try:
binance = get_binance_ticker(f"{coin}USDT")
results["binance"] = float(binance["lastPrice"])
except Exception as e:
results["binance"] = None
try:
kraken_pair = {"BTC": "XBTUSD", "ETH": "ETHUSD", "SOL": "SOLUSD"}.get(coin, f"{coin}USD")
kraken = get_kraken_ticker(kraken_pair)
results["kraken"] = kraken.get("last")
except Exception as e:
results["kraken"] = None
try:
coinbase = get_coinbase_price(f"{coin}-USD")
results["coinbase"] = coinbase.get("price")
except Exception as e:
results["coinbase"] = None
# Calculate max spread
prices = [v for v in results.values() if v is not None]
if len(prices) >= 2:
spread_pct = (max(prices) - min(prices)) / min(prices) * 100
results["spread_pct"] = round(spread_pct, 4)
results["cheapest"] = min(results, key=lambda k: results[k] or float("inf"))
results["most_expensive"] = max(results, key=lambda k: results[k] or 0)
return results
# Check BTC spread across exchanges
comparison = compare_exchange_prices("BTC")
for exchange, price in comparison.items():
if isinstance(price, float):
print(f" {exchange}: ${price:,.2f}")
print(f" Spread: {comparison.get('spread_pct', 0):.4f}%")
Technical Analysis Indicators
With OHLCV data in SQLite, you can compute indicators without any TA library:
def compute_sma(closes: list, window: int) -> list:
"""Simple Moving Average."""
sma = []
for i in range(len(closes)):
if i < window - 1:
sma.append(None)
else:
sma.append(sum(closes[i - window + 1:i + 1]) / window)
return sma
def compute_rsi(closes: list, period: int = 14) -> list:
"""Relative Strength Index."""
if len(closes) < period + 1:
return [None] * len(closes)
rsi = [None] * period
gains = []
losses = []
for i in range(1, period + 1):
delta = closes[i] - closes[i - 1]
gains.append(max(delta, 0))
losses.append(abs(min(delta, 0)))
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
for i in range(period, len(closes)):
delta = closes[i] - closes[i - 1]
gain = max(delta, 0)
loss = abs(min(delta, 0))
avg_gain = (avg_gain * (period - 1) + gain) / period
avg_loss = (avg_loss * (period - 1) + loss) / period
if avg_loss == 0:
rsi.append(100.0)
else:
rs = avg_gain / avg_loss
rsi.append(round(100 - (100 / (1 + rs)), 2))
return rsi
def analyze_symbol(conn: sqlite3.Connection, symbol: str, interval: str = "1d") -> dict:
"""Compute SMA and RSI for a stored symbol."""
rows = conn.execute(
"SELECT close FROM candles WHERE symbol = ? AND interval = ? ORDER BY open_time",
(symbol, interval),
).fetchall()
closes = [r[0] for r in rows]
if len(closes) < 20:
return {"symbol": symbol, "error": "insufficient data"}
sma20 = compute_sma(closes, 20)
sma50 = compute_sma(closes, 50)
rsi14 = compute_rsi(closes, 14)
current_price = closes[-1]
current_sma20 = sma20[-1]
current_sma50 = sma50[-1] if sma50[-1] else None
current_rsi = rsi14[-1]
signal = "NEUTRAL"
if current_sma20 and current_sma50:
if current_price > current_sma20 > current_sma50 and current_rsi and current_rsi < 70:
signal = "BULLISH"
elif current_price < current_sma20 < current_sma50 and current_rsi and current_rsi > 30:
signal = "BEARISH"
return {
"symbol": symbol,
"current_price": current_price,
"sma20": round(current_sma20, 2) if current_sma20 else None,
"sma50": round(current_sma50, 2) if current_sma50 else None,
"rsi14": current_rsi,
"signal": signal,
"candles": len(closes),
}
# Analyze stored symbols
conn = init_db()
for symbol in ["BTCUSDT", "ETHUSDT", "SOLUSDT"]:
analysis = analyze_symbol(conn, symbol)
print(f"{symbol}: ${analysis['current_price']:,.2f} | RSI: {analysis['rsi14']} | {analysis['signal']}")
conn.close()
Conclusion
CoinGecko is the easier starting point — good for market cap rankings, coin metadata, and multi-currency support without an account. Binance is better for high-resolution OHLCV and volume data, especially if you're working primarily with major pairs.
Both are free, both are stable, and neither requires auth for read-only market data. Start with the requests library, add retry logic from the beginning, and store to SQLite so you're not re-fetching the same history every run. For high-frequency monitoring across many pairs, ThorData's residential proxies give you the IP diversity needed to stay well under rate limits at scale.
Adding a third exchange like Kraken or Coinbase gives you cross-exchange spread data that's useful beyond just backup coverage — even small arbitrage gaps are worth tracking as market health indicators. And computing basic TA indicators (SMA, RSI) directly in Python means you don't need a heavy library for common signals.