AlgoLab Blog · 퀀트 · 유니버스 선정 · 2026

거래량순위 API로 종목 스캐너 만들기 — 제외필터 10자리 함정

퀀트 · 스크리닝 2026-08-17 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 봇이 매일 아침 매매할 종목을 스스로 고르게 하려면 KIS 거래량순위 API(volume-rank, tr_id = FHPST01710000)를 부르고, 제외 필터인 FID_TRGT_EXLS_CLS_CODE로 관리종목·정리매매·SPAC을 먼저 쳐 내면 됩니다. 이 파라미터에 함정이 하나 있습니다. 파라미터 설명에는 10자리라고 되어 있는데, 한국투자증권이 공개한 예제 코드는 자릿수 검증을 주석으로 막아 두고 "0을 6개 입력해야 나온다"는 취지의 메모를 남겨 두었습니다. 10자리로 보냈는데 output이 비어 오면 대부분 여기가 원인입니다. 등락률은 별도 API(ranking/fluctuation, FHPST01700000)라 파라미터가 다르고, 둘을 교집합으로 묶어야 쓸 만한 후보군이 나옵니다.

자동매매를 맡기시는 분들이 가장 많이 하는 질문이 "그래서 종목은 누가 고르나요"입니다. 전략은 "이런 신호가 뜨면 산다"까지 설명되는데 그 신호를 어느 종목에서 볼 것인가는 대개 비어 있습니다. 코스피·코스닥 2,700개를 전부 실시간 감시하는 것은 개인 계정 유량으로 불가능합니다.

그래서 실전 봇은 거의 예외 없이 2단 구조입니다. 아침에 후보를 30~50개로 줄이고(스캐너), 장중에는 그 후보만 봅니다(전략). 이 글은 앞단만, 즉 후보를 좁히는 규칙을 코드로 고정하는 방법을 다룹니다.

이 글의 순서

  1. 봇이 종목을 고르는 방식 3가지
  2. 거래량순위 API 스펙 — 파라미터 11개
  3. 제외 필터 10자리 함정
  4. 소속 구분 코드 5가지 — 무엇을 "거래량"으로 볼 것인가
  5. 등락률 순위 API와 교집합
  6. 스캐너 전체 코드
  7. 스캐너가 뽑은 종목을 그대로 사면 안 되는 이유
  8. 자주 묻는 질문

1. 봇이 종목을 고르는 방식 3가지

실무에서 쓰이는 방식은 사실상 셋뿐입니다.

방식장점한계
고정 리스트
내가 정한 20종목
단순하고 예측 가능. 백테스트와 실전이 같은 유니버스 시장이 바뀌어도 그대로. 상장폐지·거래정지가 나면 사람이 직접 고쳐야 함
조건검색식
HTS에서 만든 식을 봇이 호출
화면에서 눈으로 만들고 바로 씀. 지표 조합이 자유로움 증권사 화면에 종속. 키움 조건검색처럼 지원 여부가 증권사마다 다름
순위 API
거래량·등락률 순위를 서버에서 받음
코드로 전부 재현 가능. 필터 조건이 파라미터라 버전 관리가 됨 파라미터가 많고 문서와 실제가 어긋나는 지점이 있음

맡겨서 만드는 봇은 세 번째를 씁니다. 이유는 성능이 아니라 재현성입니다. 순위 API는 필터 조건이 전부 코드 안의 문자열이라, 나중에 성과가 이상할 때 "그날 어떤 조건으로 뽑았는지"를 그대로 되돌려 볼 수 있습니다.

2. 거래량순위 API 스펙 — 파라미터 11개

한국투자증권 KIS API의 국내주식 순위분석 그룹에 있습니다. 경로와 tr_id는 이렇습니다.

GET /uapi/domestic-stock/v1/quotations/volume-rank
tr_id: FHPST01710000        # 거래량순위

# 필수 쿼리 파라미터 11개
FID_COND_MRKT_DIV_CODE = "J"          # J:KRX  NX:NXT  UN:통합  W:ELW
FID_COND_SCR_DIV_CODE  = "20171"      # 고정값. 다른 값 넣으면 안 나옴
FID_INPUT_ISCD         = "0000"       # 0000:전체, 그 외:업종코드
FID_DIV_CLS_CODE       = "0"          # 0:전체 1:보통주 2:우선주
FID_BLNG_CLS_CODE      = "0"          # 0:평균거래량 1:거래증가율 2:평균거래회전율
                                      # 3:거래금액순 4:평균거래금액회전율
FID_TRGT_CLS_CODE      = "111111111"  # 증거금 30/40/50/60/100%, 신용보증금 30/40/50/60%
FID_TRGT_EXLS_CLS_CODE = "0000000000" # ★ 제외 필터 — 아래 3장 참고
FID_INPUT_PRICE_1      = "0"          # 가격 하한 (전체면 공란)
FID_INPUT_PRICE_2      = "1000000"    # 가격 상한 (전체면 공란)
FID_VOL_CNT            = "100000"     # 최소 거래량 (전체면 공란)
FID_INPUT_DATE_1       = ""           # 공란

많아 보이지만 봇이 매일 바꾸는 값은 세 개뿐입니다 — FID_INPUT_PRICE_1·FID_INPUT_PRICE_2(가격 구간)와 FID_VOL_CNT(최소 거래량). 나머지는 설정 파일에 넣고 손대지 않는 편이 안전합니다.

FID_COND_MRKT_DIV_CODENX·UN은 그냥 넘기지 마십시오. 대체거래소가 생긴 뒤 KRX만 본 순위와 통합 순위가 다를 수 있습니다. 주문을 어느 시장으로 보낼지와 같은 값으로 묶어 두는 편이 혼란이 적습니다.

3. 제외 필터 10자리 함정

스캐너에서 가장 중요한 파라미터는 순위 기준이 아니라 제외 조건입니다. 봇은 종목명을 읽지 않기 때문에, 필터를 걸지 않으면 정리매매 중인 종목이나 관리종목도 "거래량이 폭증한 좋은 후보"로 집어 옵니다. FID_TRGT_EXLS_CLS_CODE는 각 자리를 1(제외)/0으로 채우는 문자열이고, 설명에 적힌 순서는 다음과 같습니다.

자리제외 대상봇에 미치는 영향
1투자위험·경고·주의매수 주문 자체가 제한되거나 예탁금 요건이 붙을 수 있음
2관리종목상장폐지 심사 대상. 봇이 들고 있는 동안 거래정지 가능
3정리매매가격 제한폭이 사실상 없는 구간. 백테스트 가정이 전부 깨짐
4불성실공시공시 리스크
5우선주유동성이 얇아 슬리피지가 크게 발생
6거래정지주문이 접수되지 않음
7ETF개별주 전략을 ETF에 적용하면 전제가 다름
8ETN동일
9신용주문불가신용을 쓰는 전략이면 확인 필요
10SPAC합병 전 껍데기. 거래량만 튀는 경우가 잦음

여기가 함정입니다. 한국투자증권이 공개한 오픈API 예제 코드에는 이 값의 자릿수를 검증하는 코드가 들어 있는데, 그 검증문이 통째로 주석 처리되어 있고 바로 위에 To fix: description에 나와있는 자릿수와 다름(0 6개 입력해야 나옴)이라는 메모가 붙어 있습니다. 실제 호출 예제도 설명과 달리 6자리를 넘깁니다.

설명은 10자리인데 예제는 6자리입니다. 10자리를 보냈는데 output이 빈 배열로 오거나 아예 결과가 안 나온다면, 코드나 토큰 문제가 아니라 이 자릿수를 먼저 의심하십시오. 정답은 발행 시점의 KIS Developers 문서에서 직접 확인해야 합니다 — 순위 API의 필터 스펙은 조용히 바뀌는 축에 속합니다.

실무에서는 이렇게 처리합니다. 자릿수를 코드에 박지 말고 설정으로 빼고, 응답이 비면 폴백합니다.

EXCLUDE_PRESETS = {
    "doc10": "1111011000",   # 설명 기준 10자리: 위험·관리·정리·불성실·거래정지 제외
    "sample6": "000000",     # 공식 예제 기준 6자리
}

def fetch_volume_rank(session, preset="doc10"):
    params = dict(BASE_PARAMS)
    params["FID_TRGT_EXLS_CLS_CODE"] = EXCLUDE_PRESETS[preset]
    rows = call(session, "/uapi/domestic-stock/v1/quotations/volume-rank",
                "FHPST01710000", params)
    if not rows and preset == "doc10":
        log.warning("빈 응답 — 제외필터 자릿수 폴백 (10 -> 6)")
        return fetch_volume_rank(session, preset="sample6")
    return rows

폴백을 로그로 남기는 것이 핵심입니다. 조용히 6자리로 넘어가면 제외 조건이 사라진 상태로 스캐너가 돌게 되고, 어느 날 관리종목이 후보에 섞여 들어옵니다.

4. 소속 구분 코드 5가지 — 무엇을 "거래량"으로 볼 것인가

FID_BLNG_CLS_CODE순위를 매기는 기준입니다. 값에 따라 완전히 다른 목록이 나옵니다.

기준어떤 종목이 올라오나
0평균 거래량원래 크고 항상 많이 거래되는 종목. 목록이 잘 안 바뀜
1거래 증가율평소 대비 오늘 튄 종목. 목록이 매일 크게 바뀜
2평균 거래 회전율상장주식수 대비 손바뀜이 큰 종목
3거래 금액순단가가 높은 대형주가 유리
4평균 거래대금 회전율시가총액 대비 대금이 도는 정도

선택은 전략의 성격을 따라갑니다. 돌파·모멘텀 계열이면 1(거래 증가율), 체결을 우선한다면 3(거래 금액순)이 무난합니다. 0은 목록이 잘 안 바뀌어 고정 리스트와 비슷해집니다.

응답에는 순위 기준과 무관하게 아래 값들이 함께 들어옵니다. 스캐너의 2차 필터는 대부분 이 필드들로 만듭니다.

{
  "output": [
    {
      "hts_kor_isnm": "종목명",
      "mksc_shrn_iscd": "005930",     # 단축 종목코드
      "data_rank": "1",               # 순위
      "stck_prpr": "72300",           # 현재가
      "prdy_ctrt": "2.41",            # 전일 대비율(%)
      "acml_vol": "18342112",         # 누적 거래량
      "prdy_vol": "9120455",          # 전일 거래량
      "lstn_stcn": "5969782550",      # 상장 주식수
      "avrg_vol": "11204331",         # 평균 거래량
      "vol_inrt": "63.72",            # 거래량 증가율
      "vol_tnrt": "0.31",             # 거래량 회전율
      "acml_tr_pbmn": "1326041220000" # 누적 거래대금
    }
  ]
}

필드명이 낯설어 보여도 규칙이 있습니다. acml_은 누적, prdy_는 전일, avrg_는 평균, tr_pbmn은 거래대금, tnrt는 회전율, inrt는 증가율입니다. 이 규칙만 알면 다른 KIS API의 응답도 대부분 읽힙니다.

전체 종목 약 2,700 volume-rank 가격 구간 · 최소 거래량 제외 필터 (관리·정리·SPAC) → 상위 N fluctuation 등락률 구간 교집합 → 30종목 전략 감시 호출 1회로 여러 종목 — 여기까지는 유량 부담이 없다 후보 30개에 종목별 조회를 붙이는 순간 호출량이 30배가 된다
스캐너 2단 구조 — 순위 API로 좁히고, 좁힌 뒤에 종목별 조회를 붙인다

5. 등락률 순위 API와 교집합

거래량만으로 뽑으면 거래는 터졌는데 가격은 하한가로 붙어 있는 종목도 들어옵니다. 그래서 등락률 순위를 한 번 더 받아 교집합을 만듭니다. 이건 별도 API입니다.

GET /uapi/domestic-stock/v1/ranking/fluctuation
tr_id: FHPST01700000        # 국내주식 등락률 순위

FID_COND_MRKT_DIV_CODE = "J"       # J:KRX  NX:NXT
FID_COND_SCR_DIV_CODE  = "20170"   # 고정값 (거래량순위의 20171과 다름)
FID_INPUT_ISCD         = "0000"    # 0000:전체
FID_RANK_SORT_CLS_CODE = "0000"    # 0000:등락률순
FID_INPUT_CNT_1        = "30"      # 조회할 종목 수
FID_PRC_CLS_CODE       = "0"
FID_INPUT_PRICE_1      = "0"
FID_INPUT_PRICE_2      = "1000000"
FID_VOL_CNT            = "100000"
FID_TRGT_CLS_CODE      = "0"
FID_TRGT_EXLS_CLS_CODE = "0"
FID_DIV_CLS_CODE       = "0"
FID_RSFL_RATE1         = "0"       # 등락률 하한
FID_RSFL_RATE2         = "10"      # 등락률 상한

두 API는 파라미터 이름이 겹치지만 구성이 다릅니다. 거래량순위에는 FID_RSFL_RATE1·FID_RANK_SORT_CLS_CODE·FID_INPUT_CNT_1이 없고, 등락률 순위에는 FID_BLNG_CLS_CODE·FID_INPUT_DATE_1이 없습니다. 화면 분류 코드도 2017120170으로 다릅니다. 한쪽 코드를 복사해 tr_id만 바꾸면 인증은 통과하는데 결과가 비어 옵니다.

FID_RSFL_RATE2에 상한을 두는 것이 실무적으로 중요합니다. 상한 없이 등락률순으로 받으면 상한가 근처 종목이 상단을 차지하는데, 그런 종목은 호가가 한쪽으로 몰려 매수 주문이 체결되지 않는 경우가 많습니다. 체결되지 않는 후보는 봇 입장에서 후보가 아닙니다.

6. 스캐너 전체 코드

두 API를 부르고 pandas로 교집합을 만든 뒤, 2차 필터를 거는 최소 구현입니다. 토큰 발급은 KIS API 발급 가이드access_token을 그대로 씁니다.

import time, requests, pandas as pd

BASE = "https://openapi.koreainvestment.com:9443"
HEAD = {"authorization": f"Bearer {access_token}",
        "appkey": APP_KEY, "appsecret": APP_SECRET, "custtype": "P"}

def call(path, tr_id, params):
    r = requests.get(BASE + path, headers={**HEAD, "tr_id": tr_id},
                     params=params, timeout=5)
    body = r.json()
    if body.get("rt_cd") != "0":
        raise RuntimeError(f"{body.get('msg_cd')} {body.get('msg1')}")
    return pd.DataFrame(body.get("output") or [])

# 1) 거래량순위 — 거래 증가율 기준
vol = call("/uapi/domestic-stock/v1/quotations/volume-rank", "FHPST01710000", {
    "FID_COND_MRKT_DIV_CODE": "J", "FID_COND_SCR_DIV_CODE": "20171",
    "FID_INPUT_ISCD": "0000",      "FID_DIV_CLS_CODE": "0",
    "FID_BLNG_CLS_CODE": "1",                       # 거래 증가율
    "FID_TRGT_CLS_CODE": "111111111",
    "FID_TRGT_EXLS_CLS_CODE": EXCLUDE,              # 3장 참고
    "FID_INPUT_PRICE_1": "2000", "FID_INPUT_PRICE_2": "500000",
    "FID_VOL_CNT": "300000",     "FID_INPUT_DATE_1": "",
})
time.sleep(0.3)                                     # 유량 여유

# 2) 등락률 순위 — 0% ~ +12% 구간만
flu = call("/uapi/domestic-stock/v1/ranking/fluctuation", "FHPST01700000", {
    "FID_COND_MRKT_DIV_CODE": "J", "FID_COND_SCR_DIV_CODE": "20170",
    "FID_INPUT_ISCD": "0000",      "FID_RANK_SORT_CLS_CODE": "0000",
    "FID_INPUT_CNT_1": "100",      "FID_PRC_CLS_CODE": "0",
    "FID_INPUT_PRICE_1": "2000",   "FID_INPUT_PRICE_2": "500000",
    "FID_VOL_CNT": "300000",       "FID_TRGT_CLS_CODE": "0",
    "FID_TRGT_EXLS_CLS_CODE": "0", "FID_DIV_CLS_CODE": "0",
    "FID_RSFL_RATE1": "0",         "FID_RSFL_RATE2": "12",
})

# 3) 교집합 + 2차 필터
vol = vol.astype({"acml_tr_pbmn": "int64", "stck_prpr": "int64"})
cand = (vol[vol.mksc_shrn_iscd.isin(set(flu.stck_shrn_iscd))]
          .query("acml_tr_pbmn >= 5_000_000_000")   # 거래대금 50억 이상
          .head(30)[["mksc_shrn_iscd", "hts_kor_isnm",
                     "stck_prpr", "prdy_ctrt", "vol_inrt"]])

print(len(cand), "종목 선정")
cand.to_csv(f"universe_{pd.Timestamp.today():%Y%m%d}.csv", index=False)

마지막 줄을 빠뜨리지 마십시오. 그날의 유니버스를 파일로 남겨야 나중에 "왜 이 종목을 샀나"를 되짚을 수 있습니다.

종목코드 키 이름이 두 API에서 다릅니다 — 거래량순위는 mksc_shrn_iscd, 등락률 순위는 stck_shrn_iscd입니다. 붙일 때 가장 흔하게 터지는 지점입니다. 전체 종목코드 확보는 마스터파일 파싱에 따로 정리해 두었습니다.

유니버스 규칙을 정하는 게 더 어렵습니다

코드는 위가 전부입니다. 실제로 시간이 걸리는 건 "무엇을 후보에서 뺄 것인가"를 정하는 일이고, 그건 전략과 계좌 규모에 따라 달라집니다. 상담에서 그 기준부터 같이 잡아 드립니다.

유니버스 기준 상담하기 →

7. 스캐너가 뽑은 종목을 그대로 사면 안 되는 이유

순위 API는 "오늘 거래가 몰린 곳"을 알려 줄 뿐 "살 만한 곳"을 알려 주지 않습니다. 최소한 아래 셋은 더 봐야 합니다.

① 호가가 붙어 있는가

거래대금이 커도 매수·매도 호가 사이가 벌어져 있으면 지정가는 안 붙고 시장가는 밀립니다. 후보를 뽑은 뒤 종목별로 호가 조회 API를 불러 스프레드와 잔량을 보고 나서 진입 여부를 정하는 것이 순서입니다.

② 변동성완화장치가 걸릴 종목인가

거래 증가율 상위에는 급변동 종목이 많고, 그런 종목은 장중 VI가 발동해 2~10분간 단일가로 전환됩니다. 그동안 지정가 주문은 그대로 대기 상태가 됩니다. VI 발동 시 봇 동작을 정의해 두지 않으면 이 구간에서 주문이 쌓입니다.

③ 호출량이 감당되는가

스캐너 자체는 호출 2회입니다. 문제는 그 다음입니다. 후보 30종목에 종목별 호가 조회를 붙이면 그 순간 30회, 잔고까지 보면 더 늘어납니다. 계정 단위 유량을 넘기면 EGW00201이 돌아옵니다 — 초당 거래건수 초과에 정리해 둔 그 응답입니다. 후보 수에 상한을 두는 것은 전략적 선택이 아니라 유량 설계입니다.

코인 쪽도 구조는 같습니다. 거래소마다 순위 API 형태는 다르지만 "좁히고 나서 개별 조회를 붙인다"는 2단 구조는 동일하고, 필터 항목만 달라집니다 — 알트코인 유니버스 필터에서는 유의종목 지정과 24시간 거래대금 하한이 그 자리를 차지합니다.

자주 묻는 질문

Q. 거래량순위 API의 경로와 tr_id는 무엇인가요?

/uapi/domestic-stock/v1/quotations/volume-rank, tr_idFHPST01710000입니다. 등락률 순위는 /uapi/domestic-stock/v1/ranking/fluctuation, FHPST01700000으로 별도 API이고 화면 분류 코드도 2017120170으로 다릅니다.

Q. FID_TRGT_EXLS_CLS_CODE는 몇 자리인가요?

설명은 10자리, 공식 예제는 6자리입니다. 예제 코드의 자릿수 검증문은 주석 처리되어 있고 "0을 6개 입력해야 나온다"는 메모가 남아 있습니다. 10자리로 보내 결과가 비면 자릿수를 먼저 의심하고, 현재 값은 공식 문서에서 확인하십시오.

Q. 순위로 뽑은 종목을 그대로 매매해도 되나요?

권장하지 않습니다. 순위는 거래가 몰린 사실만 알려 주고 그 이유는 알려 주지 않습니다. 호가 스프레드·VI 이력·거래대금 하한 같은 2차 필터를 반드시 붙이십시오.

Q. 스캐너를 몇 시에 돌려야 하나요?

전일 종가 기준이면 08:30 전후, 장중 거래량을 보고 정하면 09:05~09:30에 한 번 더 돌리는 구성이 흔합니다. 휴장일에는 아예 돌지 않아야 하므로 개장일 확인이 선행되어야 합니다.

확인 캐치. 이 글의 경로·tr_id·파라미터명·응답 필드명과 제외 필터 자릿수에 관한 내용은 2026년 8월 17일 기준 한국투자증권이 공개한 오픈API 예제 코드의 명세와 주석에 근거합니다. 순위 API의 파라미터와 필터 항목은 사전 공지 없이 변경될 수 있으므로 구현 전 KIS Developers 공식 문서에서 현재 값을 확인하십시오. 본 글은 특정 종목에 대한 추천이나 수익률에 대한 예측을 담고 있지 않으며, 과거의 거래량·등락률이 미래의 결과를 보장하지 않습니다.

마무리

"어제는 잘 되던 봇이 오늘 이상한 종목을 샀다"는 사고는 전략 버그가 아니라 유니버스에서 시작되는 경우가 많습니다.

정리하면 셋입니다. 순위 API로 후보를 좁히고, 제외 필터는 자릿수를 의심하며 로그로 남기고, 좁힌 뒤에야 종목별 조회를 붙인다.

종목 선정까지 자동으로 돌리고 싶다면

유니버스 선정부터 진입·청산·로그까지 실제로 도는 상태로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기