업비트 ticker/all 전체 시세 — 소문자 krw는 400
업비트 GET /v1/ticker/all은 quote_currencies가 필수이고 대문자만 받습니다 — ?quote_currencies=krw는 HTTP 400이고, KRW로 고치면 그대로 200이 돌아옵니다. 파라미터를 빼도, 없는 통화 XXX를 넣어도 메시지가 똑같아서(Invalid parameter. Check the given value!) 응답만으로는 원인을 구분할 수 없습니다. 2026-09-08 실호출로 KRW 286개 · BTC 326개 · USDT 233개를 확인했고, /v1/ticker와 응답 필드 차집합이 0개(완전히 동일)라는 것까지 대조했습니다. 흔히 말하는 "한 번에 100개 제한"은 API가 아니라 pyupbit의 제약입니다 — /v1/ticker에 286개를 전부 넣어도 200이 옵니다.
목차
- 400이 나는 이유 — 실제 요청·응답 4종
- 실호출 응답 원문과 마켓별 종목 수
- /v1/ticker vs /v1/ticker/all 대조
- "100개 제한"은 API가 아니라 pyupbit
- 호출 한도 — Remaining-Req 헤더 원문
- 봇 유니버스에 쓸 때 — market_event는 여기 없다
- UTC와 KST가 섞여 있다
- FAQ
1. 400이 나는 이유 — 실제 요청·응답 4종
업비트 Open API에서 /v1/ticker/all은 인증이 필요 없는 시세(Quotation) 엔드포인트입니다. 그런데 400이 자주 납니다. 2026-09-08에 네 가지를 직접 호출한 결과입니다.
| 요청 | HTTP | 결과 |
|---|---|---|
/v1/ticker/all?quote_currencies=KRW | 200 | 286건 |
/v1/ticker/all?quote_currencies=krw | 400 | 소문자 불가 |
/v1/ticker/all (파라미터 없음) | 400 | 필수 파라미터 |
/v1/ticker/all?quote_currencies=XXX | 400 | 없는 통화 |
세 가지 400의 응답 본문이 전부 같습니다.
$ curl -i "https://api.upbit.com/v1/ticker/all?quote_currencies=krw"
HTTP/2 400
{"error":{"name":400,"message":"Invalid parameter. Check the given value!"}}
여기가 함정입니다 — 오타(krw)든, 파라미터 누락이든, 미지원 통화(XXX)든 error.message가 글자 하나까지 똑같습니다. 그래서 로그에 응답 본문만 찍어 두면 원인을 영영 구분할 수 없습니다. 요청 URL을 함께 남기고, 보내기 직전에 upper()를 한 번 거는 것이 가장 싼 해결입니다.
import requests
BASE = "https://api.upbit.com"
def ticker_all(quotes="KRW"):
# 대문자 강제 + 공백 제거 — 400 의 대부분이 여기서 사라진다
q = ",".join(s.strip().upper() for s in quotes.split(",") if s.strip())
r = requests.get(f"{BASE}/v1/ticker/all",
params={"quote_currencies": q}, timeout=5)
if r.status_code != 200:
# 응답 본문만으로는 원인이 구분되지 않으므로 요청 URL 을 같이 남긴다
raise RuntimeError(f"{r.status_code} {r.text} | url={r.url}")
return r.json()
print(len(ticker_all("KRW"))) # 286
print(len(ticker_all("krw, btc"))) # 612 (upper() 가 살려 준다)
업비트 공식 OpenAPI 정의에서 quote_currencies는 required: true로 되어 있고 예시 값이 KRW,BTC,USDT입니다. 파라미터가 선택이 아니라 필수라는 뜻이고, 그래서 빈 호출이 200이 아니라 400을 돌려줍니다. 이 엔드포인트(마켓 단위 현재가 조회) 자체는 2024-09-04에 추가된 비교적 최근 기능이라, 그 이전에 쓰인 예제 코드에는 아예 등장하지 않습니다.
2. 실호출 응답 원문과 마켓별 종목 수
2026-09-08 08:12 KST 기준으로 마켓별 페어 수를 세었습니다.
| quote_currencies | 페어 수 | 응답 크기 |
|---|---|---|
KRW | 286 | 226,028 B |
BTC | 326 | — |
USDT | 233 | — |
KRW,BTC | 612 | — |
쉼표로 이어 붙이면 합쳐서 한 번에 옵니다(286 + 326 = 612). 응답 한 건은 이렇게 생겼습니다 — 실제로 받은 원문입니다.
{
"market": "KRW-XRP",
"trade_date": "20260907", <- UTC 날짜
"trade_time": "231200",
"trade_date_kst": "20260908", <- 한국 날짜 (하루 차이)
"trade_time_kst": "081200",
"trade_timestamp": 1788822720856,
"opening_price": 1937.0,
"high_price": 1944.0,
"low_price": 1882.0,
"trade_price": 1902.0,
"prev_closing_price": 1936.0,
"change": "FALL",
"change_price": 34.0, <- 부호 없는 절대값
"change_rate": 0.0175619835,
"signed_change_price": -34.0, <- 부호 있는 값
"signed_change_rate": -0.0175619835,
"trade_volume": 3.94529195,
"acc_trade_price": 123371132030.78798,
"acc_trade_price_24h": 128955707722.48091,
"acc_trade_volume": 64572959.50523073,
"acc_trade_volume_24h": 67458456.23680323,
"highest_52_week_price": 4412.0,
"highest_52_week_date": "2025-09-13",
"lowest_52_week_price": 1392.0,
"lowest_52_week_date": "2026-08-18",
"timestamp": 1788822721235
}
change_price와 change_rate는 부호가 없습니다. 방향은 change(RISE·FALL·EVEN)나 signed_ 접두 필드로 봐야 합니다. 등락률로 정렬하는 코드에서 change_rate를 그대로 쓰면 하락 종목이 상승 상위에 섞여 들어옵니다 — 봇 필터에서 실제로 자주 나오는 실수입니다. 반드시 signed_change_rate를 쓰십시오.
3. /v1/ticker vs /v1/ticker/all 대조
두 응답의 키 집합을 직접 빼 봤습니다. 결과는 양방향 모두 차집합 0개 — 필드가 완전히 같습니다.
a = set(requests.get(f"{BASE}/v1/ticker/all",
params={"quote_currencies":"KRW"}).json()[0].keys())
b = set(requests.get(f"{BASE}/v1/ticker",
params={"markets":"KRW-BTC"}).json()[0].keys())
print(sorted(a - b), sorted(b - a)) # [] [] ← 필드 차이 없음
| 항목 | /v1/ticker | /v1/ticker/all |
|---|---|---|
| 필수 파라미터 | markets (종목 코드) | quote_currencies (호가 통화) |
| 종목 목록을 미리 알아야 하나 | 예 — 보통 /v1/market/all 선행 호출 | 아니오 |
| 신규 상장 자동 포함 | 아니오 (목록 갱신 필요) | 예 |
| 응답 필드 | 완전히 동일 (실측 차집합 0) | |
| 호출 한도 그룹 | group=ticker — 같은 한도를 공유 | |
| 일부 종목만 필요할 때 | 유리 (필요한 것만) | 불리 (전체가 옴) |
정리하면 "응답을 바꿔 주는 API"가 아니라 "무엇을 미리 알고 있어야 하는지를 바꿔 주는 API"입니다. 종목 열 개만 보는 봇이라면 /v1/ticker가 낫고, KRW 마켓 전체를 스캔하는 봇이라면 /v1/ticker/all이 /v1/market/all 호출을 통째로 없애 줍니다.
4. "100개 제한"은 API가 아니라 pyupbit
업비트 현재가는 한 번에 100개까지라는 말이 널리 퍼져 있습니다. 확인해 봤습니다 — KRW 마켓 286개 코드를 /v1/ticker의 markets에 전부 넣었습니다.
ms = [x["market"] for x in
requests.get(f"{BASE}/v1/market/all",
params={"is_details":"true"}).json()
if x["market"].startswith("KRW-")]
print(len(ms)) # 286
r = requests.get(f"{BASE}/v1/ticker", params={"markets": ",".join(ms)})
print(r.status_code, len(r.json()), len(r.content)) # 200 286 226028
HTTP 200에 286건이 그대로 돌아왔고, 응답 크기 226,028바이트는 ticker/all의 KRW 응답과 같았습니다. 업비트 공식 문서의 markets 설명에도 개수 상한이 적혀 있지 않습니다. 100개는 파이썬 라이브러리 pyupbit의 get_current_price가 내부에서 나눠 부르는 단위입니다.
그렇다고 markets에 긴 문자열을 넣는 것이 항상 안전하지는 않습니다. 286개 코드는 URL 길이가 3천 자를 넘어가고, 클라이언트·프록시·게이트웨이 중 어딘가에서 잘리면 원인을 찾기 어려운 부분 응답이 됩니다. 전체가 목적이면 quote_currencies 한 단어를 보내는 /v1/ticker/all이 구조적으로 안전합니다. 소요 시간도 실측에서 1회 호출 54ms vs 100개씩 3회 루프 121ms였습니다.
5. 호출 한도 — Remaining-Req 헤더 원문
업비트 공식 문서 기준 초당 최대 10회, IP 단위로 측정되며 현재가 그룹 안에서 요청 가능 횟수를 공유합니다. 실제 응답 헤더입니다.
HTTP/2 200
date: Mon, 07 Sep 2026 23:12:02 GMT
content-type: application/json;charset=UTF-8
content-length: 226099
remaining-req: group=ticker; min=600; sec=9
limit-by-ip: Yes
group=ticker가 핵심입니다. /v1/ticker와 /v1/ticker/all은 같은 그룹이라 둘을 섞어 쓴다고 한도가 늘지 않습니다. min=600은 분당 잔여, sec=9는 초당 잔여입니다. 넘기면 429가 돌아옵니다.
봇 설계 관점 — 전체 시세를 초 단위로 계속 봐야 한다면 폴링 주기를 줄이는 방향이 아니라 WebSocket ticker 구독으로 옮기는 것이 맞습니다. REST 폴링은 한도와 지연을 동시에 밀어붙이는 구조입니다. 한도 설계 자체는 API 호출 제한 설계에 따로 정리해 두었습니다. 제한값은 공지에 따라 바뀌므로 업비트 공식 문서를 기준으로 삼으십시오.
6. 봇 유니버스에 쓸 때 — market_event는 여기 없다
/v1/ticker/all이 종목 목록 호출을 없애 준다고 해서 /v1/market/all이 필요 없어지는 것은 아닙니다. 유의종목·주의종목 표시인 market_event는 /v1/market/all?is_details=true에만 있습니다. ticker/all 응답에는 그 필드가 없습니다.
| 필요한 것 | 어디서 오나 |
|---|---|
| 전체 페어의 현재가·거래대금·52주 고저 | /v1/ticker/all |
한글명 korean_name, 유의·주의 market_event | /v1/market/all?is_details=true |
그래서 실무 조합은 이렇게 갈립니다 — 종목 목록과 market_event는 느리게(하루 1~2회) 갱신하고, 시세는 ticker/all로 자주 받습니다.
from datetime import datetime, timedelta
_universe, _fetched = {}, None
def universe():
"""유의종목 제외 목록 — 하루 한 번만 갱신"""
global _universe, _fetched
if _fetched is None or datetime.now() - _fetched > timedelta(hours=12):
rows = requests.get(f"{BASE}/v1/market/all",
params={"is_details": "true"}).json()
_universe = {r["market"] for r in rows
if r["market"].startswith("KRW-")
and not r.get("market_event", {}).get("warning")}
_fetched = datetime.now()
return _universe
def scan(min_value=1_000_000_000):
"""거래대금 10억 이상 + 유의종목 제외 → 등락률 정렬"""
ok = universe()
rows = [t for t in ticker_all("KRW")
if t["market"] in ok and t["acc_trade_price_24h"] >= min_value]
return sorted(rows, key=lambda t: t["signed_change_rate"], reverse=True)
필터 조건을 무엇으로 잡을지는 업비트 종목 리스트 API로 짜는 알트코인 봇 필터에 market_event·거래대금·상장 기간까지 나눠 정리해 두었습니다. 이 글은 그중 시세를 받아 오는 한 칸만 다룹니다.
7. UTC와 KST가 섞여 있다
1절의 응답 원문을 다시 보면 trade_date가 20260907인데 trade_date_kst는 20260908입니다. 버그가 아니라 앞의 두 필드가 UTC입니다.
한국 시각 자정~오전 9시 사이에는 UTC 날짜가 하루 전입니다. 이 구간에 trade_date로 일자를 집계하면 하루가 통째로 밀립니다. 코인은 24시간 거래되므로 봇이 도는 시간대와 정확히 겹치는 구간이라 특히 위험합니다. 일자 기준이 필요하면 trade_date_kst·trade_time_kst를 쓰십시오.
덧붙여 change·change_rate 같은 변동 지표는 업비트 공식 문서상 전일 종가 기준으로 산출됩니다. 24시간 시장에서 "전일"의 경계가 언제인지는 거래소가 정하는 값이므로, 봇이 자체 계산하는 일봉 등락률과 숫자가 안 맞아도 오류가 아닙니다. 둘 중 하나를 기준으로 정해 두고 섞지 마십시오. 24시간 누적은 acc_trade_price_24h·acc_trade_volume_24h가 따로 있습니다.
8. FAQ
?quote_currencies=krw가 400이 납니다.
소문자라서 그렇습니다. KRW로 고치면 200입니다. 파라미터 누락·없는 통화도 같은 메시지를 내므로 응답만으로는 구분되지 않습니다. 요청 직전에 upper()를 거십시오.
/v1/ticker와 /v1/ticker/all은 뭐가 다른가요?
응답 필드는 완전히 같습니다(실측 차집합 0). 다른 것은 markets(종목을 미리 알아야 함) vs quote_currencies(몰라도 됨)이고, 후자는 신규 상장이 자동 포함됩니다.
한 번에 100개까지만 조회되나요?
API 제한이 아닙니다. /v1/ticker에 KRW 286개를 전부 넣어도 200에 286건이 옵니다. 100개는 pyupbit.get_current_price의 분할 단위입니다. 다만 URL이 3천 자를 넘으므로 전체가 목적이면 ticker/all이 안전합니다.
얼마나 자주 호출할 수 있나요?
공식 문서 기준 초당 10회·IP 단위, 현재가 그룹 공유입니다. 헤더에 group=ticker; min=600; sec=9가 찍힙니다. /v1/ticker와 한도를 나눠 씁니다. 초 단위 감시라면 WebSocket이 맞습니다.
trade_date와 trade_date_kst가 다릅니다.
앞이 UTC입니다. KST 자정~오전 9시에는 하루 차이가 납니다. 일자 집계에는 _kst 필드를 쓰십시오.
마무리
이 엔드포인트에서 시간을 잡아먹는 것은 스펙이 어려워서가 아니라 오류 메시지가 원인을 안 알려 주기 때문입니다. krw 세 글자 때문에 나는 400과, 파라미터를 빼서 나는 400과, 오타 난 통화 코드 때문에 나는 400이 글자 하나까지 같습니다. 요청 URL을 로그에 남기고 upper()를 한 번 거는 것 — 이 두 줄이 이 API에서 나올 문제의 대부분을 없앱니다.
고지 — 알고랩(퀀트웍스)은 투자자문업·투자일임업을 영위하지 않으며, 고객의 규칙을 코드로 옮기는 도구 제공만 합니다. 이 글은 특정 종목·전략을 추천하지 않고 수익을 보장하지 않습니다. 본문의 응답·헤더·종목 수는 2026-09-08 실호출로 확인한 값이며, 시세 숫자는 그 시점의 스냅숏입니다. API 스펙·호출 한도는 공지에 따라 바뀌므로 업비트 Open API 공식 문서를 기준으로 삼으십시오.