Free Crypto Price Data: Collecting Market Data with Public APIs
The first thing you need to build a crypto bot isn't a strategy—it's data. Fortunately most exchanges open up market data for free, with no sign-up and no API key. This article walks through collecting that data from public APIs all the way to a CSV file.
⚠️ This article is for educational and informational purposes. Cryptocurrency is extremely volatile. This is not investment advice, and you alone are responsible for any losses.
1. What Needs a Key and What Doesn't
Exchange APIs split into two kinds. Understanding just this distinction sorts out half your confusion.
- Public API — prices, candles, order book, trade history. No key required. Anyone can call it.
- Private API — balance lookups, orders, withdrawals. Requires a key and a signature, and directly affects your account.
For the data-collection and backtesting stage, public APIs are entirely sufficient. There's no reason to create a key while you're practicing, and a key you never create can never leak.
2. Fetching the Current Price (Upbit)
import requests
def upbit_price(market="KRW-BTC"):
url = "https://api.upbit.com/v1/ticker"
res = requests.get(url, params={"markets": market}, timeout=10)
res.raise_for_status()
d = res.json()[0]
return {
"market": d["market"],
"price": d["trade_price"],
"change": d["signed_change_rate"] * 100, # % vs previous day
}
print(upbit_price())
# {'market': 'KRW-BTC', 'price': 95_400_000, 'change': 1.23}
You can also fetch several symbols at once. It's far faster than looping one at a time, and safer against rate limits.
markets = "KRW-BTC,KRW-ETH,KRW-XRP"
res = requests.get("https://api.upbit.com/v1/ticker",
params={"markets": markets}, timeout=10)
for d in res.json():
print(d["market"], d["trade_price"])
3. Fetching Historical Candles — Backtest Fuel
A candle (OHLCV) bundles open, high, low, close, and volume over a fixed interval. Backtesting essentially runs on top of this data.
def upbit_candles(market="KRW-BTC", unit=60, count=200):
"""unit: minutes (1,3,5,15,60,240). count: 200 max"""
url = f"https://api.upbit.com/v1/candles/minutes/{unit}"
res = requests.get(url, params={"market": market, "count": count},
timeout=10)
res.raise_for_status()
return res.json() # returned newest → oldest
rows = upbit_candles()
print(len(rows), rows[0]["candle_date_time_kst"])
The 200-per-call limit matters here. For longer ranges you have to walk backward, using the to parameter to say "before this timestamp," and stitch the pages together.
import time
def collect_history(market="KRW-BTC", unit=60, pages=5):
all_rows, to = [], None
for _ in range(pages):
params = {"market": market, "count": 200}
if to:
params["to"] = to # request data before this timestamp
res = requests.get(
f"https://api.upbit.com/v1/candles/minutes/{unit}",
params=params, timeout=10)
res.raise_for_status()
rows = res.json()
if not rows:
break
all_rows += rows
to = rows[-1]["candle_date_time_utc"] # timestamp of the oldest candle
time.sleep(0.2) # spacing between calls (required)
return all_rows
Don't drop the time.sleep(0.2). Most exchanges enforce a calls-per-second limit and block you with a 429 when you exceed it. At 0.2 seconds you're at five calls per second, which is broadly safe.
4. Binance — Overseas Prices and Longer Ranges
Binance's public API returns up to 1000 rows per call and offers generous history. The response is a JSON array, so the structure differs a bit.
def binance_klines(symbol="BTCUSDT", interval="1h", limit=1000):
url = "https://api.binance.com/api/v3/klines"
res = requests.get(url, params={
"symbol": symbol, "interval": interval, "limit": limit,
}, timeout=10)
res.raise_for_status()
# [open_time, open, high, low, close, volume, close_time, ...]
return [{
"time": k[0],
"open": float(k[1]),
"high": float(k[2]),
"low": float(k[3]),
"close": float(k[4]),
"volume": float(k[5]),
} for k in res.json()]
Note that prices arrive as strings. Skip the float() conversion and you end up doing string comparisons like "95400" > "9600", which quietly produce wrong answers.
5. Saving to CSV
import pandas as pd
rows = collect_history("KRW-BTC", unit=60, pages=5)
df = pd.DataFrame([{
"date": r["candle_date_time_kst"],
"open": r["opening_price"],
"high": r["high_price"],
"low": r["low_price"],
"close": r["trade_price"],
"volume": r["candle_acc_trade_volume"],
} for r in rows])
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date").drop_duplicates("date") # chronological + dedup
df.to_csv("btc_1h.csv", index=False, encoding="utf-8-sig")
print(f"saved {len(df)} rows, {df['date'].min()} ~ {df['date'].max()}")
Always include the deduplication. Stitching pages together pulls the same candle twice at the boundaries, and leaving those in distorts your backtest results.
6. Rules for Collecting
- Respect the call spacing — on a 429, pause and retry. Retrying endlessly gets you blocked.
- Save what you fetch — reuse the local file instead of re-downloading for every experiment. Better for the server and for you.
- Check the timezone — mix KST and UTC and a silent nine-hour error creeps in.
- Read the terms of service — even public APIs often restrict commercial redistribution.
Summary
- Prices and candles are collectible from public APIs with no key
- Request multiple symbols in one call
- For long ranges, page backward with
toand sleep between calls - Binance sends numbers as strings — check your type conversion
- Sort and deduplicate before saving, and unify the timezone
Once the data is in hand, strategy experimentation can finally begin. Conversely, if the data is dirty, no result built on top of it can be trusted.
댓글
댓글 쓰기