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 to and 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.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기