Overview
This guide shows practical, free ways to fetch daily crypto price and volume data in Python for Algorithmic Trading with Python. You’ll get minimal code, trade-offs, rate-limit notes, and pitfalls to avoid.
Quickstart
- Choose a source
- CoinGecko: simple, no key, daily prices and total volume (not strict exchange OHLCV volume).
- Binance: exchange-grade OHLCV for pairs like BTCUSDT, no key, strict rate weights.
- CryptoCompare: unified OHLCV across many pairs, free key required.
- Alpha Vantage: free key, slower limits, daily OHLCV in USD.
- yfinance: convenient wrapper for daily bars like BTC-USD; unofficial.
- Install packages
pip install requests pandas yfinance
- Run the minimal working example (CoinGecko)
- Great for quick daily close + total volume in fiat.
Minimal working example: CoinGecko daily close + volume
import requests
import pandas as pd
def coingecko_daily(coin_id="bitcoin", vs="usd", days=180):
url = f"https://api.coingecko.com/api/v3/coins/{coin_id}/market_chart"
params = {"vs_currency": vs, "days": days, "interval": "daily"}
r = requests.get(url, params=params, timeout=30)
r.raise_for_status()
data = r.json()
prices = pd.DataFrame(data["prices"], columns=["ts", "close"]) # close-like spot
volumes = pd.DataFrame(data["total_volumes"], columns=["ts", "volume"])
df = prices.merge(volumes, on="ts")
df["date"] = pd.to_datetime(df["ts"], unit="ms", utc=True).dt.tz_localize(None).dt.date
df = df[["date", "close", "volume"]].sort_values("date").reset_index(drop=True)
return df
if __name__ == "__main__":
df = coingecko_daily("bitcoin", "usd", days=365)
print(df.tail())
# Example: simple daily returns
df["ret"] = df["close"].pct_change()
print(df[["date", "close", "volume", "ret"]].tail())
Notes
- volume is total traded volume in the quote currency (here USD-equivalent), aggregated across many venues.
- For strict OHLCV per exchange/pair, use Binance (below).
More free sources (with short Python snippets)
1) Binance (exchange OHLCV for pairs; no key)
import requests, pandas as pd
def binance_klines(symbol="BTCUSDT", limit=1000):
url = "https://api.binance.com/api/v3/klines"
params = {"symbol": symbol, "interval": "1d", "limit": limit}
r = requests.get(url, params=params, timeout=30)
r.raise_for_status()
raw = r.json()
cols = [
"open_time","open","high","low","close","volume",
"close_time","qav","trades","tbbav","tbqav","ignore"
]
df = pd.DataFrame(raw, columns=cols)
for c in ["open","high","low","close","volume"]:
df[c] = df[c].astype(float)
df["date"] = pd.to_datetime(df["open_time"], unit="ms").dt.date
return df[["date","open","high","low","close","volume"]]
# Example
# df = binance_klines("ETHUSDT", limit=1000)
Notes
- volume is base-asset volume (e.g., BTC for BTCUSDT).
- Symbol format is BASEQUOTE (e.g., BTCUSDT, ETHUSDT).
2) CryptoCompare (free key; unified OHLCV)
import os, requests, pandas as pd
def cryptocompare_histoday(fsym="BTC", tsym="USD", limit=2000, api_key=None):
url = "https://min-api.cryptocompare.com/data/v2/histoday"
params = {"fsym": fsym, "tsym": tsym, "limit": limit}
headers = {"authorization": f"Apikey {api_key}"} if api_key else {}
r = requests.get(url, params=params, headers=headers, timeout=30)
r.raise_for_status()
j = r.json()
if j.get("Response") != "Success":
raise RuntimeError(j.get("Message", "API error"))
d = j["Data"]["Data"]
df = pd.DataFrame(d)
df["date"] = pd.to_datetime(df["time"], unit="s").dt.date
df = df.rename(columns={"volumefrom": "volume_base", "volumeto": "volume_quote"})
return df[["date","open","high","low","close","volume_base","volume_quote"]]
# Example
# api_key = os.getenv("CRYPTOCOMPARE_KEY")
# df = cryptocompare_histoday("BTC", "USD", 2000, api_key)
Notes
- volume_base is in fsym (BTC); volume_quote is in tsym (USD).
- Requires free API key via header authorization.
3) Alpha Vantage (free key; daily OHLCV in USD)
import requests, pandas as pd
def alphavantage_daily(symbol="BTC", market="USD", api_key="demo"):
url = "https://www.alphavantage.co/query"
params = {
"function": "DIGITAL_CURRENCY_DAILY",
"symbol": symbol,
"market": market,
"apikey": api_key,
}
r = requests.get(url, params=params, timeout=30)
r.raise_for_status()
j = r.json()
ts = j.get("Time Series (Digital Currency Daily)")
if not ts:
raise RuntimeError(j.get("Note") or j.get("Error Message") or "No data")
df = pd.DataFrame.from_dict(ts, orient="index")
df.index = pd.to_datetime(df.index)
cols = {
"1a. open (USD)": "open",
"2a. high (USD)": "high",
"3a. low (USD)": "low",
"4a. close (USD)": "close",
"5. volume": "volume",
}
out = df[list(cols.keys())].rename(columns=cols).astype(float)
out["date"] = out.index.date
return out.reset_index(drop=True)[["date","open","high","low","close","volume"]]
# Example
# df = alphavantage_daily("ETH", "USD", api_key="YOUR_KEY")
Notes
- Free tier is rate-limited; batch smartly.
4) yfinance (convenient daily bars; unofficial)
import pandas as pd
import yfinance as yf
def yf_daily(ticker="BTC-USD", period="max"):
df = yf.download(ticker, interval="1d", period=period, auto_adjust=False)
df = df.rename(columns=str.lower).reset_index()
df["date"] = pd.to_datetime(df["date"]).dt.date
return df[["date","open","high","low","close","volume"]]
# Example
# df = yf_daily("BTC-USD", period="5y")
Notes
- Subject to upstream changes; validate against a reference.
Quick comparison
Source | Key? | Daily OHLC | Daily Volume | Historical depth | Notes |
---|---|---|---|---|---|
CoinGecko | No | Close only | Total volume (quote) | Up to max | Simple, aggregated across venues |
Binance | No | Yes | Base-asset volume | Deep per pair | Exchange-specific pairs |
CryptoCompare | Yes | Yes | Base + quote | Long (limit=2000) | Unified across many markets |
Alpha Vantage | Yes | Yes | Base volume | Long | Tight rate limits |
yfinance | No | Yes | Quote volume | Long | Unofficial, may change |
Common pitfalls
- Symbol mismatches
- BTCUSDT (Binance) vs BTC-USD (yfinance) vs BTC/USD (generic). Map explicitly per source.
- Volume definitions vary
- Exchange OHLCV often uses base-asset volume; aggregate feeds may present quote volume. Do not mix without conversion.
- Timezones and daily cutoffs
- Daily bars are typically UTC-based. Align to UTC when merging across sources.
- Missing/partial last bar
- The current day’s bar may be incomplete; filter out today if you need closed bars only.
- Rate limits
- Expect HTTP 429. Add retries with exponential backoff and caching.
Performance notes
- Batch and cache
- Cache responses on disk (e.g., parquet) keyed by source/symbol/date to avoid re-downloading.
- Use vectorized pandas ops
- Convert types once and keep DataFrames tidy to avoid repeated parsing.
- Retry with backoff
import time, requests
def get_with_backoff(url, params=None, headers=None, attempts=5):
delay = 1.0
for i in range(attempts):
try:
r = requests.get(url, params=params, headers=headers, timeout=30)
if r.status_code == 429 and i < attempts - 1:
time.sleep(delay); delay *= 2; continue
r.raise_for_status()
return r
except requests.RequestException:
if i == attempts - 1:
raise
time.sleep(delay); delay *= 2
- Minimize JSON-to-DataFrame overhead
- Select and cast columns once; avoid holding unused fields.
Example workflow (end-to-end)
- Pull OHLCV from Binance for backtesting a USDT pair.
- Pull CoinGecko daily close for broad market sanity checks.
- Align on date, inner-join by date, drop today’s partial bar.
- Compute features (returns, rolling vol) and export to parquet.
Tiny FAQ
- Q: I need OHLCV for BTCUSD without an account. What should I use?
- A: Binance BTCUSDT daily klines are free and require no key.
- Q: Why don’t CoinGecko OHLC match exchange candles?
- A: CoinGecko aggregates across venues; definitions and timing differ.
- Q: How can I get more than 1000 days from Binance?
- A: Page backward using the startTime/endTime parameters or store incrementally.
- Q: My last bar is zero volume; is that a bug?
- A: Often it’s an incomplete current-day bar. Filter to completed days for signals.