업데이트: 2026년 11월 3일. Binance 현물·USDⓈ-M 선물과 Bybit V5의 공개 시장 데이터 엔드포인트를 공식 문서와 대조해 코드와 설명을 고쳤습니다. 파라미터 한도와 레이트 리밋은 바뀔 수 있으니 적용 전에 아래 링크한 공식 문서를 확인하세요.
공개 데이터는 키 없이 받지만, 규칙은 거래소마다 다르다
캔들(OHLCV), 펀딩레이트, 오더북 스냅샷은 Binance와 Bybit 모두 API 키 없이 조회할 수 있는 공개 시장 데이터입니다. 키가 필요한 것은 잔고 조회, 주문, 포지션 관리 같은 계정 기능입니다. 리서치용 수집기에는 처음부터 키를 넣지 않는 편이 안전합니다. 키를 써야 하는 단계라면 읽기 전용 권한과 IP 제한을 걸고, 출금 권한은 켜지 않습니다.
어려운 부분은 키가 아니라 거래소별 차이입니다. 같은 “1시간 봉”이라도 interval 표기, 정렬 순서, 한 번에 받을 수 있는 개수, 과다 요청 시 응답 코드가 다릅니다. 이 차이를 수집 단계에서 정규화하지 않으면 백테스트가 조용히 틀립니다.
| 데이터 | Binance | Bybit V5 |
|---|---|---|
| 캔들 | GET /api/v3/klines (현물), limit 기본 500·최대 1000, weight 2 |
GET /v5/market/kline, limit 기본 200·최대 1000, 최신 봉이 먼저 |
| 펀딩레이트 이력 | GET /fapi/v1/fundingRate (USDⓈ-M), limit 기본 100·최대 1000, 오름차순 |
GET /v5/market/funding/history, limit 기본·최대 200 |
| 오더북 스냅샷 | GET /api/v3/depth, limit 기본 100·최대 5000, weight 5–250 |
GET /v5/market/orderbook, linear 기본 25·최대 1000 |
| 과다 요청 | HTTP 429 뒤에도 계속 보내면 418(IP 차단, 2분–3일) | 기본 IP당 5초 600회, 초과 시 403과 10분 이상 대기 |
공식 문서 기준 요약(2026년 11월 3일 확인). Binance 한도는 IP 기준이며, 현재 한도는 /api/v3/exchangeInfo의 rateLimits에서 확인합니다.
요청 구성: 엔드포인트와 파라미터를 문서에 고정한다
비공식 래퍼 라이브러리는 편하지만, 내부에서 어떤 엔드포인트와 기본값을 쓰는지 한 단계 가려집니다. 수집기의 핵심 경로는 공식 문서의 경로와 파라미터 이름을 그대로 쓰고, URL 생성과 네트워크 호출을 분리해 두면 테스트가 쉽습니다. 아래 코드는 요청을 보내지 않고 URL만 만듭니다.
from urllib.parse import urlencode
BINANCE_SPOT = "https://api.binance.com"
BINANCE_USDM = "https://fapi.binance.com"
BYBIT = "https://api.bybit.com"
def build_url(base, path, **params):
query = {k: v for k, v in params.items() if v is not None}
return f"{base}{path}?{urlencode(query)}"
def binance_klines(symbol, interval, start_ms=None, limit=1000):
return build_url(BINANCE_SPOT, "/api/v3/klines", symbol=symbol,
interval=interval, startTime=start_ms, limit=limit)
def binance_funding(symbol, start_ms=None, end_ms=None, limit=1000):
return build_url(BINANCE_USDM, "/fapi/v1/fundingRate", symbol=symbol,
startTime=start_ms, endTime=end_ms, limit=limit)
def binance_depth(symbol, limit=100):
return build_url(BINANCE_SPOT, "/api/v3/depth", symbol=symbol, limit=limit)
def bybit_klines(symbol, interval, category="linear", end_ms=None, limit=1000):
return build_url(BYBIT, "/v5/market/kline", category=category,
symbol=symbol, interval=interval, end=end_ms, limit=limit)
def bybit_funding(symbol, category="linear", end_ms=None, limit=200):
return build_url(BYBIT, "/v5/market/funding/history", category=category,
symbol=symbol, endTime=end_ms, limit=limit)
url = binance_klines("BTCUSDT", "1h", start_ms=1767225600000)
assert url == ("https://api.binance.com/api/v3/klines?symbol=BTCUSDT"
"&interval=1h&startTime=1767225600000&limit=1000")
assert "startTime" not in binance_funding("BTCUSDT")
assert bybit_klines("BTCUSDT", "60").startswith(
"https://api.bybit.com/v5/market/kline?category=linear&symbol=BTCUSDT&interval=60")
assert bybit_funding("BTCUSDT").endswith("limit=200")
두 거래소의 파라미터 이름이 다릅니다. Binance 캔들은 startTime·endTime, Bybit 캔들은 start·end를 씁니다. interval도 Binance는 1h, Bybit는 분 단위 숫자 60입니다. Bybit 캔들의 category는 생략하면 linear로 처리되므로, 현물을 원하면 spot을 명시해야 합니다. Binance 현물은 api.binance.com 외에 공개 시장 데이터 전용 data-api.binance.vision 호스트도 문서에 있습니다.
캔들: 정렬 순서와 시간 단위를 먼저 맞춘다
Binance 캔들 응답은 12개 원소 배열(시가 시각, OHLC, 거래량, 종가 시각, 거래대금, 체결 수, 테이커 매수량 등)이고, 가격과 수량은 문자열입니다. Bybit는 7개 원소(시작 시각, OHLC, 거래량, 거래대금) 문자열 배열이며 최신 봉이 먼저 옵니다. 파서는 두 형태를 같은 UTC 인덱스와 float 열로 바꾸고, 정렬을 명시적으로 합니다.
import pandas as pd
# 문서의 응답 형태를 본뜬 예시 값 (실제 시세 아님)
binance_payload = [
[1767225600000, "100.0", "101.0", "99.5", "100.5", "12.0",
1767229199999, "1206.0", 340, "6.0", "603.0", "0"],
[1767229200000, "100.5", "102.0", "100.0", "101.5", "8.0",
1767232799999, "812.0", 210, "5.0", "507.5", "0"],
]
bybit_payload = {
"retCode": 0, "retMsg": "OK",
"result": {"category": "linear", "symbol": "BTCUSDT", "list": [
["1767229200000", "100.6", "102.1", "100.1", "101.4", "9.0", "910.0"],
["1767225600000", "100.1", "101.1", "99.6", "100.6", "11.0", "1105.0"],
]},
}
COLS = ["open", "high", "low", "close", "volume"]
def parse_binance_klines(rows):
df = pd.DataFrame([r[:6] for r in rows], columns=["open_time", *COLS])
df["open_time"] = pd.to_datetime(df["open_time"].astype("int64"), unit="ms", utc=True)
df[COLS] = df[COLS].astype(float)
return df.set_index("open_time")
def parse_bybit_klines(payload):
if payload["retCode"] != 0:
raise RuntimeError(payload["retMsg"])
rows = [r[:6] for r in payload["result"]["list"]]
df = pd.DataFrame(rows, columns=["open_time", *COLS])
df["open_time"] = pd.to_datetime(df["open_time"].astype("int64"), unit="ms", utc=True)
df[COLS] = df[COLS].astype(float)
return df.set_index("open_time").sort_index() # Bybit은 최신 봉이 먼저 온다
a = parse_binance_klines(binance_payload)
b = parse_bybit_klines(bybit_payload)
assert a.index.is_monotonic_increasing and b.index.is_monotonic_increasing
assert list(a.index) == list(b.index)
assert str(a.index[0]) == "2026-01-01 00:00:00+00:00"
assert a["close"].iloc[-1] == 101.5 and b["close"].iloc[0] == 100.6
거래량 의미도 확인해야 합니다. Binance 현물의 volume은 기초자산 수량이고, Bybit는 문서상 volume과 turnover(호가 통화 기준)를 따로 줍니다. 거래소 간 거래량을 합치거나 비교하기 전에 단위를 맞춥니다. 봉 시각은 시작 시각 기준이므로, 신호를 그 봉의 종가 이후에만 쓸 수 있다는 점도 함께 기록합니다. 이용 가능 시점 관리는 포인트인타임 데이터 칼럼과 같은 원리입니다.
긴 이력은 페이지로 받고, 겹침과 공백을 검사한다
한 번에 받을 수 있는 캔들은 Binance 현물·Bybit 모두 최대 1000개입니다. 1시간 봉 1년치(약 8,760개)는 여러 번 나눠 받아야 합니다. 다음 페이지의 시작점은 마지막 봉의 종가 시각 + 1ms로 잡고, 다 받은 뒤 중복과 공백을 셉니다. 아래는 가짜 응답 함수로 페이지 로직을 검증하는 예시입니다.
HOUR_MS = 3_600_000
def fake_klines(start_ms, limit, last_open_ms):
"""오프라인 테스트용: startTime부터 최대 limit개의 1시간 봉을 돌려준다."""
opens = range(start_ms, last_open_ms + 1, HOUR_MS)
return [[t, "1", "1", "1", "1", "0", t + HOUR_MS - 1] for t in opens][:limit]
def paginate(fetch, start_ms, limit=1000):
rows, cursor = [], start_ms
while True:
page = fetch(cursor, limit)
if not page:
break
rows.extend(page)
cursor = page[-1][6] + 1 # 마지막 봉 close_time + 1ms
if len(page) < limit:
break
return rows
def check_continuity(rows, step_ms):
opens = [r[0] for r in rows]
dupes = len(opens) - len(set(opens))
gaps = [(a, b) for a, b in zip(opens, opens[1:]) if b - a != step_ms]
return dupes, gaps
start = 1767225600000 # 2026-01-01 00:00 UTC
last = start + 2_499 * HOUR_MS # 2,500개 봉
rows = paginate(lambda s, n: fake_klines(s, n, last), start)
assert len(rows) == 2_500
assert check_continuity(rows, HOUR_MS) == (0, [])
holed = rows[:10] + rows[11:] # 봉 하나가 빠진 경우
dupes, gaps = check_continuity(holed, HOUR_MS)
assert dupes == 0 and len(gaps) == 1
공백이 발견되면 자동으로 채우지 말고 기록해 둡니다. 거래소 점검, 상장 전 기간, 수집 실패는 서로 다른 원인이고 처리 방법도 다릅니다. Bybit는 최신순으로 오므로 end를 과거 방향으로 옮기며 받는 편이 자연스럽습니다.
수년치 이력이 필요하면 REST 반복 호출보다 Binance가 공개하는 data.binance.vision 일·월 단위 파일이 효율적입니다. 각 zip에 .CHECKSUM 파일이 있어 무결성을 확인할 수 있습니다. 다만 이 저장소 안내에 따르면 2025년 1월 1일 이후 현물 파일의 타임스탬프는 마이크로초이므로, REST(기본 밀리초)와 섞을 때 단위를 맞춰야 합니다.
펀딩레이트: 정산 주기를 가정하지 말고 데이터로 확인한다
예전 판의 이 글은 펀딩 주기를 8시간으로 가정하고 21개 기록을 “7일 평균”이라고 불렀습니다. 이 가정은 모든 계약에 맞지 않습니다. Bybit 문서는 심볼마다 펀딩 주기가 다르다고 명시하고, Binance도 GET /fapi/v1/fundingInfo에 fundingIntervalHours 필드를 둡니다. 주기가 다른 계약의 “1회 정산 비율”을 그대로 비교하면 차이가 왜곡됩니다.
import pandas as pd
# 문서의 응답 형태를 본뜬 예시 값 (실제 펀딩레이트 아님)
binance_funding = [
{"symbol": "BTCUSDT", "fundingRate": "0.00010000", "fundingTime": 1767225600000,
"markPrice": "100.0", "rateType": "Regular"},
{"symbol": "BTCUSDT", "fundingRate": "0.00008000", "fundingTime": 1767254400000,
"markPrice": "101.0", "rateType": "Regular"},
{"symbol": "BTCUSDT", "fundingRate": "-0.00002000", "fundingTime": 1767283200000,
"markPrice": "99.0", "rateType": "Regular"},
]
bybit_funding = {"retCode": 0, "retMsg": "OK", "result": {"category": "linear", "list": [
{"symbol": "XYZUSDT", "fundingRate": "0.0002", "fundingRateTimestamp": "1767240000000"},
{"symbol": "XYZUSDT", "fundingRate": "0.0001", "fundingRateTimestamp": "1767225600000"},
{"symbol": "XYZUSDT", "fundingRate": "0.0003", "fundingRateTimestamp": "1767211200000"},
]}}
def normalize(rows, venue, time_key):
df = pd.DataFrame({
"venue": venue,
"symbol": [r["symbol"] for r in rows],
"funding_time": pd.to_datetime([int(r[time_key]) for r in rows], unit="ms", utc=True),
"rate": [float(r["fundingRate"]) for r in rows],
})
df = df.sort_values("funding_time").reset_index(drop=True)
step = df["funding_time"].diff().dt.round("min").median()
df["interval_h"] = step / pd.Timedelta(hours=1)
return df
a = normalize(binance_funding, "binance", "fundingTime")
b = normalize(bybit_funding["result"]["list"], "bybit", "fundingRateTimestamp")
assert a["interval_h"].iloc[0] == 8.0 # 이 예시에서는 8시간
assert b["interval_h"].iloc[0] == 4.0 # 이 예시에서는 4시간
assert b["funding_time"].is_monotonic_increasing
# 주기가 다르면 정산 1회당 비율을 바로 비교할 수 없다: 하루 기준으로 맞춘다
a["rate_per_day"] = a["rate"] * 24 / a["interval_h"]
b["rate_per_day"] = b["rate"] * 24 / b["interval_h"]
assert abs(a["rate_per_day"].iloc[0] - 0.0003) < 1e-12
assert abs(b["rate_per_day"].iloc[-1] - 0.0012) < 1e-12
몇 가지 문서상 동작도 코드에 반영합니다. Binance fundingRate는 기간을 주지 않으면 최근 200건을 오름차순으로 주고, fundingInfo와 함께 IP당 5분 500회 한도를 공유합니다. 응답의 rateType은 일반 펀딩이면 Regular입니다. Bybit는 startTime만 보내면 오류이고, endTime만 보내거나 둘 다 생략하면 그 시점까지 최대 200건을 돌려줍니다.
데이터에서 추정한 주기는 기록이 하나뿐이거나 중간에 주기가 바뀌면 틀립니다. 운영에서는 fundingInfo(Binance)나 instruments-info(Bybit)의 주기 정보를 함께 저장하고, 추정값과 다르면 경고합니다. 펀딩레이트 수준을 “과열” 같은 신호로 바꾸는 기준값은 이 글에서 제시하지 않습니다. 그 기준은 별도의 검증 대상입니다.
오더북 스냅샷은 한 시점의 기록이다
REST 오더북은 요청 순간의 스냅샷입니다. 큰 주문이 쌓인 가격대가 곧 지지·저항이라는 해석은 데이터가 보장하지 않습니다. 호가는 체결 없이 취소될 수 있습니다. 스냅샷에서 일관되게 얻을 수 있는 것은 중간가, 스프레드, 일정 범위 안의 수량 불균형 같은 기술 통계입니다.
# 문서의 응답 형태를 본뜬 예시 값 (실제 호가 아님)
binance_depth = {
"lastUpdateId": 1027024,
"bids": [["99.90", "2.0"], ["99.80", "5.0"], ["99.00", "40.0"]],
"asks": [["100.10", "1.5"], ["100.20", "4.0"], ["101.00", "30.0"]],
}
bybit_book = {"retCode": 0, "retMsg": "OK", "result": {
"s": "BTCUSDT", "b": [["99.90", "2.0"], ["99.80", "5.0"]],
"a": [["100.10", "1.5"], ["100.20", "4.0"]],
"ts": 1767225600123, "u": 18521288, "seq": 7961638724, "cts": 1767225600100}}
def to_levels(pairs):
return [(float(p), float(q)) for p, q in pairs]
def book_summary(bids, asks, band_bps=50):
best_bid, best_ask = bids[0][0], asks[0][0]
if best_bid >= best_ask:
raise ValueError("crossed book: snapshot is inconsistent")
mid = (best_bid + best_ask) / 2
lo, hi = mid * (1 - band_bps / 1e4), mid * (1 + band_bps / 1e4)
bid_qty = sum(q for p, q in bids if p >= lo)
ask_qty = sum(q for p, q in asks if p <= hi)
return {
"mid": mid,
"spread_bps": (best_ask - best_bid) / mid * 1e4,
"imbalance": (bid_qty - ask_qty) / (bid_qty + ask_qty),
}
s1 = book_summary(to_levels(binance_depth["bids"]), to_levels(binance_depth["asks"]))
r = bybit_book["result"]
s2 = book_summary(to_levels(r["b"]), to_levels(r["a"]))
assert s1["mid"] == 100.0 and round(s1["spread_bps"], 6) == 20.0
assert round(s1["imbalance"], 6) == round((7.0 - 5.5) / 12.5, 6) # 50bp 밖 호가 제외
assert s1 == s2 # 같은 호가면 같은 요약
Binance 현물 depth는 limit에 따라 weight가 5(1–100)에서 250(1001–5000)까지 커집니다. 깊은 호가를 자주 받으면 한도를 빠르게 씁니다. Bybit 응답의 ts는 시스템 생성 시각, cts는 매칭 엔진 시각이며, RPI 주문은 포함하지 않는다고 문서에 적혀 있습니다. 연속 호가가 필요하면 REST 스냅샷 반복이 아니라 WebSocket 스트림과 스냅샷 동기화 절차를 따로 구현해야 합니다.
운영: 레이트 리밋, 재시도, 저장 규칙
과다 요청 처리는 거래소마다 다릅니다. Binance는 429를 받으면 물러나야 하고, 무시하고 계속 보내면 418로 IP가 차단됩니다. 두 응답 모두 Retry-After 헤더를 줍니다. 응답 헤더 X-MBX-USED-WEIGHT-*로 현재 사용량도 볼 수 있습니다. Bybit는 기본 IP당 5초 600회를 넘으면 403을 주고, 모든 세션을 닫고 10분 이상 기다리라고 안내합니다.
# 실행하지 않은 예시: 실제 네트워크 요청을 보낸다. URL은 위의 요청 구성 함수로 만든다.
import json
import time
import urllib.error
import urllib.request
def get_json(url, max_tries=5):
for attempt in range(max_tries):
try:
with urllib.request.urlopen(url, timeout=10) as resp:
used = {k: v for k, v in resp.headers.items()
if k.lower().startswith("x-mbx-used-weight")} # Binance만 해당
return json.load(resp), used
except urllib.error.HTTPError as err:
if err.code == 418 or (err.code == 403 and "bybit" in url):
raise # Binance IP 차단, Bybit 과다 요청: 수집을 멈추고 사람이 확인한다
if err.code == 429:
time.sleep(int(err.headers.get("Retry-After") or 2 ** attempt * 5))
continue
raise
raise RuntimeError(f"gave up after {max_tries} tries: {url}")
이 코드는 실행하지 않았습니다. 엔드포인트와 헤더 이름은 공식 문서와 대조했지만, 실제 응답 코드와 한도는 계정·지역·시점에 따라 다를 수 있습니다.
저장은 덮어쓰기보다 추가 방식이 재현에 유리합니다. 원본 응답, 수집 시각, 요청 URL을 남기고, (venue, symbol, open_time) 같은 키로 중복을 제거한 정제 테이블을 따로 만듭니다. 스케줄러는 cron이나 systemd 타이머로 충분하며, 같은 구간을 두 번 받아도 결과가 같도록 만드는 것이 스케줄러 선택보다 중요합니다.
참고로 alphaware.xyz 홈 티커는 거래소 REST를 직접 호출하지 않습니다. Cloudflare 출구 IP에서 Binance 공개 REST가 403을 반환해, 같은 출처 /api/quotes가 Yahoo Finance 차트 API로 BTC·ETH와 매크로 지표를 읽습니다. 이 글의 코드는 리서치용 수집 예시이며 라이브 티커 구현이 아닙니다.
수집한 봉을 변동성 거리로 쓰는 방법은 ATR 칼럼에, 그 규칙을 검증하는 방법은 과적합 방지 칼럼에 있습니다.
Binance 가입하기 — 이 레퍼럴 링크로 가입하면 거래 수수료 할인이 적용됩니다.
자료와 적용 범위
- Binance 현물 시장 데이터 엔드포인트:
klines·depth의 파라미터, 기본값, weight, 응답 필드 순서. - Binance 현물 API 일반 정보: 기본 호스트,
data-api.binance.vision, 타임스탬프 단위. - Binance 현물 API 한도: IP 기준 한도, 429·418,
Retry-After. - Binance USDⓈ-M 펀딩레이트 이력: 기본·최대 limit, 오름차순, 5분 500회 한도,
rateType. - Bybit V5 캔들, 펀딩레이트 이력, 오더북: 파라미터, 정렬, 응답 필드.
- Bybit 레이트 리밋: IP당 5초 600회, 403 처리.
- binance-public-data: 대량 이력 파일, CHECKSUM, 2025년 이후 현물 마이크로초 타임스탬프.
- 코드 예시는 문서의 응답 형태를 본뜬 가상 값으로만 검증했습니다. 이 글은 특정 지표의 수익성이나 펀딩레이트 기반 신호의 유효성을 주장하지 않습니다.
