KIS 공매도 일별추이 FHPST04830000 — 확인할 5가지
한국투자증권 KIS Open API에서 종목별 공매도 일별 데이터는 GET /uapi/domestic-stock/v1/quotations/daily-short-sale, tr_id FHPST04830000 하나로 받습니다. 파라미터는 시장코드 FID_COND_MRKT_DIV_CODE(J)·종목코드 FID_INPUT_ISCD·시작일 FID_INPUT_DATE_1·종료일 FID_INPUT_DATE_2 네 개이고, 응답은 현재가 묶음 output1과 일별 행 배열 output2로 옵니다. 봇에 넣기 전 확인할 것은 다섯 가지 — 모의투자 미제공, 연속조회(tr_cont) 처리 없음, 잔고가 아닌 체결 수량, 비중 단위 미기재, 2023-11 ~ 2025-03 공매도 금지 구간입니다. (공식 저장소 open-trading-api 2026-10-07 main 기준)
목차
- 한눈에 보는 명세
- 첫 호출 — requests 코드
- 응답 필드 — output1 과 output2
- 확인 1 — 모의투자에서는 안 된다
- 확인 2 — 연속조회가 없다, 기간을 쪼갠다
- 확인 3 — '잔고'가 아니라 그날 체결된 수량이다
- 확인 4 — 비중(%) 단위는 직접 대조한다
- 확인 5 — 공매도 금지 구간을 잘라 낸다
- 시장 전체가 필요하면 — FHPST04820000
- 자주 묻는 질문
이 글은 KIS 신용잔고 API — 융자·대주·대차 구분에서 떼어 낸 스포크입니다. 신용잔고 글이 "빌린 돈·빌린 주식이 얼마나 쌓였나"를 다뤘다면, 이 글은 그중 공매도로 실제 체결된 흐름 하나만 봅니다. 키움증권 쪽 같은 데이터는 키움 REST API 공매도 추이 ka10014에 정리돼 있습니다.
1. 한눈에 보는 명세
| 항목 | 값 |
|---|---|
| API 이름 | [국내주식] 시세분석 > 국내주식 공매도 일별추이 [국내주식-134] |
| 메서드 · URL | GET /uapi/domestic-stock/v1/quotations/daily-short-sale |
tr_id | FHPST04830000 (Postman 설명란: "[실전투자] FHPST04830000" — 모의 TR 표기 없음) |
| 헤더 | authorization(Bearer access_token) · appkey · appsecret · tr_id · custtype(개인 P) |
| 쿼리 | FID_COND_MRKT_DIV_CODE(예제 J) · FID_INPUT_ISCD(6자리) · FID_INPUT_DATE_1 · FID_INPUT_DATE_2(YYYYMMDD) |
| 응답 | output1 = 객체 1개 · output2 = 일별 배열 |
| 예제 함수 | examples_llm/domestic_stock/daily_short_sale/daily_short_sale.py → daily_short_sale() |
날짜 두 개는 함수 시그니처에서 기본값 ""인 선택 인자인데, 같은 저장소의 MCP/Kis Trading MCP/configs/domestic_stock.json에는 "required": true로 적혀 있습니다. 비워 두면 어떤 구간이 돌아오는지 문서에 없으므로 봇에서는 항상 두 날짜를 명시하는 편이 안전합니다.
2. 첫 호출 — requests 코드
공식 예제는 kis_auth.py의 ka._url_fetch()로 감싸 두었지만, 구조를 보려면 requests로 직접 부르는 쪽이 읽기 쉽습니다. 키는 코드에 쓰지 말고 환경변수로 읽습니다.
import os, requests, pandas as pd
BASE = "https://openapi.koreainvestment.com:9443" # 실전 도메인 (모의 미제공 — 4장)
headers = {
"content-type": "application/json; charset=utf-8",
"authorization": f"Bearer {os.environ['KIS_ACCESS_TOKEN']}",
"appkey": os.environ["KIS_APPKEY"],
"appsecret": os.environ["KIS_APPSECRET"],
"tr_id": "FHPST04830000",
"custtype": "P",
}
params = {
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": "005930",
"FID_INPUT_DATE_1": "20240301",
"FID_INPUT_DATE_2": "20240328",
}
r = requests.get(f"{BASE}/uapi/domestic-stock/v1/quotations/daily-short-sale",
headers=headers, params=params, timeout=10)
body = r.json()
if body.get("rt_cd") != "0": # HTTP 200 이어도 실패일 수 있다
raise RuntimeError(f"{body.get('msg_cd')} {body.get('msg1')}")
snap = pd.DataFrame(body["output1"], index=[0]) # 객체 → 1행
daily = pd.DataFrame(body["output2"]) # 배열 → 일별 행
위 쿼리 값(005930, 2024-03-01 ~ 03-28)은 공식 예제 chk_daily_short_sale.py와 Postman 샘플 실전계좌_POSTMAN_샘플코드_v2.6.json의 값을 그대로 옮긴 것입니다. 성공 판정은 HTTP 코드가 아니라 rt_cd로 합니다 — 판정 방법과 흔한 msg_cd는 KIS API 에러코드 정리에 있습니다.
3. 응답 필드 — output1 과 output2
예제의 COLUMN_MAPPING에는 27개 키가 있지만, 그중 prdy_vrss·prdy_vrss_sign·prdy_ctrt·acml_vol 네 개는 두 번씩 적혀 있습니다. 파이썬 딕셔너리는 중복 키를 하나로 합치므로 실제 매핑은 23개이고, 이 이름들이 output1과 output2 양쪽에 같은 이름으로 존재한다는 뜻입니다.
COLUMN_MAPPING = {
'stck_prpr': '주식 현재가',
'prdy_vrss': '전일 대비', # ← 1번째
...
'prdy_vol': '전일 거래량',
'stck_bsop_date': '주식 영업 일자',
'stck_clpr': '주식 종가',
'prdy_vrss': '전일 대비', # ← 2번째 (같은 키)
...
'ssts_cntg_qty': '공매도 체결 수량',
'ssts_vol_rlim': '공매도 거래량 비중',
매핑 순서로 보면 앞의 여섯 개(stck_prpr ~ prdy_vol)가 현재가 묶음 output1, stck_bsop_date부터가 일별 행 output2로 읽힙니다. 다만 저장소가 출력별로 나눠 적어 두지는 않았으니, 첫 호출에서 snap.columns·daily.columns를 찍어 실제 열을 확인하십시오. 봇이 쓸 일별 필드는 이렇습니다.
| 필드 | 의미 (예제 매핑) | 봇에서 쓰는 곳 |
|---|---|---|
stck_bsop_date | 주식 영업 일자 | 인덱스(YYYYMMDD 문자열) |
stck_clpr · acml_vol | 종가 · 누적 거래량 | 비중 직접 계산의 분모 |
ssts_cntg_qty | 공매도 체결 수량 | 그날 공매도 거래량 |
ssts_vol_rlim | 공매도 거래량 비중 | 필터 기준값 (단위는 7장) |
ssts_tr_pbmn · ssts_tr_pbmn_rlim | 공매도 거래 대금 · 대금 비중 | 대금 기준 필터 |
acml_ssts_cntg_qty · acml_ssts_cntg_qty_rlim | 누적 공매도 체결 수량 · 비중 | 조회 구간 누적 (6장) |
stnd_vol_smtn · stnd_tr_pbmn_smtn | 기준 거래량 합계 · 기준 거래대금 합계 | 누적 비중의 분모로 추정 — 대조 필요 |
stck_oprc · stck_hgpr · stck_lwpr · avrg_prc | 시가("주식 시가2") · 고가 · 저가 · 평균가격 | 일봉 보조 |
값은 전부 문자열로 옵니다. 예제의 NUMERIC_COLUMNS = []가 비어 있어 숫자 변환도 해 주지 않으므로, 비교 전에 pd.to_numeric(errors="coerce")를 직접 거쳐야 합니다.
4. 확인 1 — 모의투자에서는 안 된다
공식 저장소의 제공 여부 표 세 곳이 같은 말을 합니다.
legacy/README.md— "|[국내주식]시세분석|국내주식 공매도 일별추이| |" → 모의투자 제공 여부 칸이 비어 있음legacy/postman/README.md— 모의 칸 공란, 실전 칸 ⭕- Postman 헤더 설명 —
tr_id설명이 "[실전투자] FHPST04830000" 뿐
모의 도메인(openapivts)으로 개발하다가 이 TR만 실패하는 이유가 이것입니다. 시세분석 TR은 주문이 없으니 실전 앱키로 조회만 하는 구성이 일반적입니다. 모의에서 안 되는 다른 TR 목록은 KIS 모의투자 한계 — 안 되는 API 10가지에 모아 두었습니다.
5. 확인 2 — 연속조회가 없다, 기간을 쪼갠다
같은 공매도 묶음의 순위 함수 short_sale()(FHPST04820000)는 tr_cont·depth·max_depth(기본 10) 인자로 다음 페이지를 재귀 호출합니다. 그런데 일별추이 daily_short_sale()는 이렇게 부릅니다.
res = ka._url_fetch(API_URL, tr_id, "", params) # tr_cont 자리에 "" 고정
연속조회를 아예 하지 않는다는 뜻입니다. 한 번에 몇 행까지 오는지는 예제·data.csv 어디에도 적혀 있지 않습니다. 그래서 긴 기간은 짧게 쪼개 부르고, 돌아온 첫 날짜가 요청 시작일에 닿았는지 검사하는 방식이 안전합니다.
import time
def fetch_short_sale(code, start, end, months=1):
out = []
for s in pd.date_range(start, end, freq=f"{months}MS"):
e = min(s + pd.DateOffset(months=months) - pd.Timedelta(days=1), pd.Timestamp(end))
params.update(FID_INPUT_ISCD=code,
FID_INPUT_DATE_1=s.strftime("%Y%m%d"),
FID_INPUT_DATE_2=e.strftime("%Y%m%d"))
b = requests.get(URL, headers=headers, params=params, timeout=10).json()
rows = pd.DataFrame(b.get("output2", []))
if not rows.empty and rows["stck_bsop_date"].min() > s.strftime("%Y%m%d"):
print("warn: 앞부분이 잘렸을 수 있음", code, s.date(), rows["stck_bsop_date"].min())
out.append(rows)
time.sleep(0.06) # 초당 호출 제한 여유 — EGW00201 방지
return pd.concat(out).drop_duplicates("stck_bsop_date").sort_values("stck_bsop_date")
월 첫날이 휴장일이면 경고가 한 번씩 뜰 수 있으니 1~3영업일 차이는 무시해도 됩니다. 호출 간격은 EGW00201 초당 거래건수 초과 글의 기준을 따릅니다.
6. 확인 3 — '잔고'가 아니라 그날 체결된 수량이다
응답 23개 필드 어디에도 공매도 잔고가 없습니다. ssts_cntg_qty는 그날 공매도로 체결된 수량이고, 다음 날 상환됐는지는 알려 주지 않습니다. 이름에 "누적"이 붙은 acml_ssts_cntg_qty도 체결 수량을 합친 값으로 읽히지, 아직 갚지 않은 물량이 아닙니다(키움 ka10014의 누적공매도량도 같은 구조였습니다). "공매도 잔고가 늘면 진입 보류" 같은 규칙을 이 TR로 만들면 처음부터 다른 데이터를 보는 셈입니다.
잔고에 가까운 지표가 필요하면 출처가 다릅니다. 주식을 빌린 쪽은 KIS의 대차거래 TR(신용잔고 글 5장)에서, 공매도 잔고 자체는 한국거래소 정보데이터시스템(KRX)의 공매도 통계에서 확인합니다. 공시 기준·시차는 거래소 공식 안내를 확인하십시오.
7. 확인 4 — 비중(%) 단위는 직접 대조한다
ssts_vol_rlim은 "공매도 거래량 비중"이라고만 적혀 있고 단위(%, 소수)와 분모가 없습니다. 봇의 필터 임계값을 "비중 5 이상"으로 둘지 "0.05 이상"으로 둘지가 여기서 갈리므로, 첫 데이터에서 직접 계산해 맞춰 봅니다.
num = ["ssts_cntg_qty", "acml_vol", "ssts_vol_rlim", "ssts_tr_pbmn", "ssts_tr_pbmn_rlim"]
daily[num] = daily[num].apply(pd.to_numeric, errors="coerce")
daily["my_rlim"] = daily["ssts_cntg_qty"] / daily["acml_vol"] * 100
gap = (daily["my_rlim"] - daily["ssts_vol_rlim"]).abs()
print(daily[["stck_bsop_date", "ssts_cntg_qty", "acml_vol", "ssts_vol_rlim", "my_rlim"]].head())
print("max gap:", gap.max()) # 0 근처면 '%' 단위·분모=당일 거래량
차이가 0 근처면 단위가 %이고 분모가 당일 거래량이라는 뜻입니다. 차이가 크게 나면 분모가 acml_vol이 아닌 것(예: 정규장만 집계한 거래량)이므로, 임계값을 정하기 전에 ssts_tr_pbmn_rlim(대금 비중)과 같은 방식으로 한 번 더 맞춰 보십시오.
8. 확인 5 — 공매도 금지 구간을 잘라 낸다
금융위원회는 2023년 11월 6일부터 국내 증시 공매도를 전면 금지했고, 약 17개월 뒤인 2025년 3월 31일 전면 재개했습니다. 이 구간의 일별 데이터는 시장조성 같은 예외 거래만 남아 평소와 성격이 다릅니다. 백테스트에서 이 기간을 다른 기간과 한 시계열로 이으면 "공매도 비중이 낮을 때 수익이 좋았다" 같은 가짜 관계가 생깁니다.
d = pd.to_datetime(daily["stck_bsop_date"], format="%Y%m%d")
ban = (d >= "2023-11-06") & (d <= "2025-03-30")
daily["regime"] = "normal"
daily.loc[ban, "regime"] = "ban"
daily.loc[d >= "2025-03-31", "regime"] = "resumed" # 재개 이후는 따로 본다
print(daily.groupby("regime")["ssts_vol_rlim"].describe())
재개 이후를 금지 이전과도 따로 두는 이유는 제도가 바뀌었기 때문입니다(전산 차단 시스템 의무화 등). 같은 숫자라도 제도가 다르면 같은 의미라고 단정할 수 없습니다. 제도 내용은 금융위원회 보도자료로 확인하십시오.
9. 시장 전체가 필요하면 — FHPST04820000
종목 하나가 아니라 "오늘 공매도 비중 상위 종목"을 보려면 순위분석의 국내주식 공매도 상위종목을 씁니다. GET /uapi/domestic-stock/v1/ranking/short-sale, tr_id FHPST04820000입니다.
| 파라미터 | 값 (data.csv 원문 요약) |
|---|---|
fid_cond_scr_div_code | 20482 (Unique key) |
fid_input_iscd | 0000 전체 · 0001 코스피 · 1001 코스닥 · 2001 코스피200 · 4001 KRX100 · 3003 코스닥150 |
fid_period_div_code | D 일 · M 월 |
fid_input_cnt_1 | D: 0=1일 · 1=2일 · 2=3일 · 3=4일 · 4=1주 · 9=2주 · 14=3주 / M: 1~3개월 |
tr_cont | 연속조회 지원 (예제 재귀 max_depth 10) |
순위로 후보를 좁히고, 후보 종목만 FHPST04830000으로 일별 흐름을 받는 2단 구조가 호출 수를 아낍니다. 순위 TR 공통 함정(시장코드 검증, 건수 미기재)은 시가총액 순위 FHPST01740000 글과 같습니다.
봇에 넣을 때 — 공매도 비중은 "그날 무슨 일이 있었나"를 보여 주는 거래 결과이지 방향 신호가 아닙니다. 실무에서는 진입 신호가 아니라 제외 필터(비중이 평소보다 급증한 날은 신규 진입 보류)로 쓰는 경우가 많고, 체결강도·거래원·프로그램매매 필터와 같은 자리에 둡니다. 특정 종목의 매매를 권하지 않으며, 필터 임계값은 반드시 자신의 데이터로 검증하십시오.
10. 자주 묻는 질문
KIS API로 공매도 데이터를 받으려면 어떤 TR을 쓰나요?
종목별 일별 흐름은 국내주식 공매도 일별추이 GET /uapi/domestic-stock/v1/quotations/daily-short-sale, tr_id FHPST04830000을 씁니다. 시장 전체 상위 종목은 공매도 상위종목 /uapi/domestic-stock/v1/ranking/short-sale, tr_id FHPST04820000입니다.
FHPST04830000은 모의투자에서 되나요?
공식 저장소의 legacy README와 Postman README 모두 모의투자 제공 칸이 비어 있고, Postman tr_id 설명도 실전투자만 적혀 있습니다. 실전 앱키로 조회만 하는 구성을 권합니다.
공매도 일별추이 응답에 공매도 잔고가 있나요?
없습니다. ssts_cntg_qty는 그날 체결된 공매도 수량이고 acml_ssts_cntg_qty도 체결 수량의 누적입니다. 잔고는 한국거래소 공매도 통계 등 별도 출처에서 확인해야 합니다.
오래된 기간을 한 번에 받을 수 있나요?
공식 예제 함수는 연속조회(tr_cont)를 처리하지 않고, 한 번에 오는 행 수도 문서에 없습니다. 한 달 단위처럼 짧게 나눠 호출하고 돌아온 첫 날짜가 요청 시작일에 닿았는지 확인하는 방식이 안전합니다.