키움 0F 당일거래원 — FID 57개와 증감의 함정
키움 REST API 실시간 0F(주식당일거래원)는 종목당 매도·매수 상위 5개 거래원을 FID 57개로 보내 줍니다. 거래원 5순위 × 양쪽 × 5항목 50개에 외국계 추정합 6개와 거래소구분 337이 더해집니다. 등록은 {"trnm":"REG","data":[{"item":["005930"],"type":["0F"]}]} 형태입니다. 다만 명세 응답 예시의 2~4위 '거래원별증감'은 바로 윗 순위 수량과의 차이와 정확히 일치합니다. 증감의 기준을 명세가 밝히지 않으니 신호로 쓰려면 거래원 코드별로 수량 변화를 직접 계산하십시오. (키움 저장소 main c7a3d25, 2026-11-06 확인. 파서는 명세 예시로만 실행했고 실서버 수신은 하지 않았습니다.)
REST의 ka10040(당일주요거래원)을 몇 초마다 부르다가 유량 제한에 걸리면 다음 후보가 0F입니다. 이 글은 0F 하나만 다룹니다. 웹소켓 접속과 LOGIN·PING은 키움 웹소켓 REG·FID 가이드를 먼저 보십시오.
목차
- FID 57개 — 번호가 붙어 있지 않다
- REG 패킷 — 명세 예시를 복사하면 안 된다
- 거래원별증감이 수상하다
- 파서와 실행 결과
- 외국계 추정합과 337
- 재접속 — ka10040으로 초기값을 채운다
1. FID 57개 — 번호가 붙어 있지 않다
kiwoom_api_spec.json의 주식당일거래원(0F) 응답을 그대로 옮겼습니다. 한 순위의 값이 나란히 있지 않고 항목별로 다섯 칸씩 띄엄띄엄 있어서, 순위 번호를 더해 찾아야 합니다.
| 항목 | 매도 1~5위 | 매수 1~5위 | ka10040 대응 필드 |
|---|---|---|---|
| 거래원명 | 141~145 | 151~155 | sel_trde_ori_N / buy_trde_ori_N |
| 거래원코드 | 146~150 | 156~160 | sel_trde_ori_cd_N / buy_trde_ori_cd_N |
| 수량(1주) | 161~165 | 171~175 | sel_trde_ori_qty_N / buy_trde_ori_qty_N |
| 별증감(부호 포함) | 166~170 | 176~180 | sel_trde_ori_irds_N / buy_trde_ori_irds_N |
| 색깔 | 271~275 | 281~285 | 없음 |
나머지 7개는 다음과 같습니다.
| FID | 명세 한글명 | FID | 명세 한글명 |
|---|---|---|---|
261 | 외국계매도추정합 | 262 | 외국계매도추정합변동 |
263 | 외국계매수추정합 | 264 | 외국계매수추정합변동 |
267 | 외국계순매수추정합 | 268 | 외국계순매수변동 |
337 | 거래소구분 |
271대의 색깔은 예시 값이 !!!!이고 명세에 설명이 없습니다. 파서에서 버려도 됩니다.
2. REG 패킷 — 명세 예시를 복사하면 안 된다
0s와 마찬가지로 명세의 요청 예시가 깨져 있습니다. data가 null이고 항목이 - item이라는 이상한 키로 빠져 있습니다.
# 명세 request_example (이대로 보내지 말 것)
{ "trnm": "REG", "grp_no": "1", "refresh": "1", "data": null, "- item": "005930", "- type": "0F" }
# 공식 예제 subscribe_domestic_stock_today_brokers_async.py
{ "trnm": "REG", "grp_no": "1", "refresh": "1",
"data": [{ "item": ["005930"], "type": ["0F"] }] }
item은 거래소별 종목코드입니다. 명세 설명에 따라 KRX039490, NXT039490_NX, SOR039490_AL로 씁니다.refresh는1입니다.0은 기존 등록을 해지하므로0B·0D를 쓰는 소켓에서 시세가 끊길 수 있습니다.- 공식 예제의 주석
등록("0"이면 해제)은 명세의REG·REMOVE와 맞지 않습니다. 해지는"trnm": "REMOVE"로 보내십시오. - 2026-07-02 기준 키움 공지(seqid 49)의 실시간 등록 상한은 200종목입니다. 다른 수치는 키움 429 유량 제한에 있습니다.
3. 거래원별증감이 수상하다
이 글을 쓰며 찾은 가장 중요한 점입니다. 명세 설명은 "단위: 1주, 부호가 포함된 숫자"뿐이고 무엇 대비 증감인지 적혀 있지 않습니다. 그래서 응답 예시의 숫자로 계산해 봤습니다.
| 매도 순위 | 코드 | 수량 FID | 수량 | 별증감 | 윗 순위와 수량 차이 |
|---|---|---|---|---|---|
| 1 | 993 | 161 | 9,350,409 | +8,593,585 | 없음 |
| 2 | 994 | 162 | 9,341,349 | -9,060 | -9,060 |
| 3 | 991 | 163 | 9,305,540 | -35,809 | -35,809 |
| 4 | 992 | 164 | 757,643 | -8,547,897 | -8,547,897 |
| 5 | 005 | 165 | 100 | 0 | -757,543 |
매수 쪽도 같습니다. 2~4위의 별증감 -5,935, -13,918, -8,537,310이 윗 순위와의 차이와 모두 일치합니다. 1위와 5위는 맞지 않습니다. 또 4위 수량이 757,643주인데 증감이 -8,547,897주라면, '이 거래원의 직전 수량 대비 변화'로 읽기가 어렵습니다.
결론을 내리지는 못합니다. 예시 하나의 숫자이고, 코드 993·994도 일반 회원사 코드처럼 보이지 않아 테스트성 데이터일 가능성이 있습니다. 그래도 근거 없이 166을 "이 증권사가 방금 판 수량"으로 신호에 넣으면 안 됩니다. 실서버에서 메시지를 모아 확인하기 전에는 다음처럼 직접 계산하십시오.
prev = {} # (종목, 쪽, 거래원코드) → 직전 수량
def my_delta(stock, side, code, qty):
key = (stock, side, code)
delta = qty - prev[key] if key in prev else None # 첫 수신은 비교 대상 없음
prev[key] = qty
return delta
순위는 바뀌므로 키를 순위가 아니라 거래원코드로 잡습니다. 수량이 5위 밖으로 밀린 거래원은 값이 오지 않아 변화를 볼 수 없다는 점도 함께 기억하십시오.
4. 파서와 실행 결과
예시의 1~4위는 거래원명이 비어 있고 코드만 있습니다. 표시에는 코드를 기본으로 쓰고 이름은 ka10102 회원사 목록으로 채우는 편이 안전합니다. 수량에는 +·-가 올 수 있고 int()는 둘 다 처리합니다.
SIDES = {"sell": (141, 161, 166, 146), "buy": (151, 171, 176, 156)}
def parse_0f(values):
out = {}
for side, (nm, qty, chg, cd) in SIDES.items():
rows = []
for i in range(5):
rows.append({
"rank": i + 1,
"name": values.get(str(nm + i), ""),
"code": values.get(str(cd + i), ""),
"qty": int(values.get(str(qty + i)) or 0),
"chg": int(values.get(str(chg + i)) or 0),
})
out[side] = rows
out["foreign_net"] = int(values.get("267") or 0)
return out
명세의 응답 예시를 넣어 실행한 출력입니다.
sell
1 993 (빈값) qty=9350409 chg=+8593585 윗순위와차이=None
2 994 (빈값) qty=9341349 chg=-9060 윗순위와차이=-9060
3 991 (빈값) qty=9305540 chg=-35809 윗순위와차이=-35809
4 992 (빈값) qty=757643 chg=-8547897 윗순위와차이=-8547897
5 005 미래에셋 qty=100 chg=+0 윗순위와차이=-757543
buy
1 993 (빈값) qty=9321128 chg=+8557163 윗순위와차이=None
2 991 (빈값) qty=9315193 chg=-5935 윗순위와차이=-5935
3 994 (빈값) qty=9301275 chg=-13918 윗순위와차이=-13918
4 992 (빈값) qty=763965 chg=-8537310 윗순위와차이=-8537310
5 005 미래에셋 qty=50100 chg=+0 윗순위와차이=-713865
5. 외국계 추정합과 337
261·263·267과 그 변동값은 이름 그대로 추정입니다. 외국인 전체 순매수의 확정치로 쓰면 안 됩니다. 확정치에 가까운 투자자별 수치는 다른 TR에서 보며, 외국계 창구 순위는 ka10037 외국계창구매매상위를 보십시오. 응답 예시는 여섯 값이 모두 0이라 예시로 계산식을 검증할 수 없었습니다. 267이 263에서 261을 뺀 값인지는 실서버에서 확인하십시오.
337은 예시 값이 2인데 설명이 비어 있습니다. 명세의 실시간 항목을 전부 훑어 보니 337이 있는 항목은 0F뿐이고 값표는 어디에도 없었습니다. 2가 어느 시장인지 추측하지 말고, 시장 구분은 등록한 item의 접미사(_NX, _AL)로 관리하십시오.
6. 재접속 — ka10040으로 초기값을 채운다
파서의 표는 0F 메시지를 받아야 채워지므로 접속 직후나 재접속 뒤에는 비어 있습니다. 메시지가 언제 오는지 주기도 명세에 없습니다. 그래서 먼저 REST로 초기값을 받습니다.
0F | ka10040 | ka10002 | |
|---|---|---|---|
| 방식 | 웹소켓 | REST /api/dostk/rkinfo | REST /api/dostk/stkinfo |
| 거래원 순위 | 양쪽 5위 | 양쪽 5위 | 양쪽 5위 |
| 별증감 | 있음 | 있음(irds) | 없음 |
| 외국계 추정합 | 있음 | 명세 40개 필드에 없음 | 없음 |
| 현재가·등락 | 없음 | 없음 | 있음 |
- 소켓 접속과
LOGIN응답을 확인합니다. REG를 보낸 뒤return_code0을 확인합니다.ka10040을 한 번 불러 코드별 수량 표를 채웁니다.- 이후
0F수신분으로 표를 덮어씁니다. 재접속할 때마다 1~3을 반복합니다.
순서를 바꿔 REST를 먼저 부르면 그 사이의 변경을 놓칠 수 있습니다. 재접속 처리 전반은 봇 장애 복구 플레이북에 있습니다. 어느 증권사가 강한지를 하루치로 보려면 ka10039 증권사별매매상위, 대량 체결을 보려면 ka10052 거래원순간거래량이 맞습니다.
알고랩은 프로그램 제작 도구를 제공하며 투자자문업·투자일임업을 영위하지 않습니다. 거래원 수량은 창구 기준 정보이고 매매 주체나 향후 가격을 뜻하지 않습니다. 명세는 바뀔 수 있으니 wss://api.kiwoom.com:10000 실서버 또는 wss://mockapi.kiwoom.com:10000 모의 서버에서 실제 수신을 확인한 뒤 주문 로직에 연결하십시오.
자주 묻는 질문
0F를 등록할 때 item에는 무엇을 넣나요?
거래소별 종목코드입니다. 명세 설명 기준 KRX는 039490, NXT는 039490_NX, SOR은 039490_AL 형식입니다. 공식 예제는 item과 type을 리스트로 보냅니다.
거래원별증감을 그대로 매매 신호로 써도 되나요?
권하지 않습니다. 명세가 기준을 밝히지 않았고, 응답 예시의 2~4위 값은 바로 윗 순위 수량과의 차이와 일치합니다. 실서버에서 확인하기 전에는 거래원 코드별 수량 변화를 직접 계산하십시오.
0F와 ka10040 중 무엇을 써야 하나요?
함께 씁니다. ka10040은 접속 직후 초기값을 한 번 받는 데, 0F는 이후 변경을 받는 데 씁니다. 외국계 추정합은 명세상 0F에만 있고 ka10040 필드에는 없습니다.
모의투자 서버에서도 0F를 받을 수 있나요?
명세에 모의투자 도메인 wss://mockapi.kiwoom.com:10000이 적혀 있습니다. 다만 이 글에서는 서버에 접속해 수신을 확인하지 않았으므로 직접 확인이 필요합니다.
실시간 거래원 데이터를 봇에 넣고 싶다면
키움 REST API 웹소켓 수신, 재접속 복원, 거래원 데이터 가공을 전략에 맞춰 맞춤 제작합니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기