AlgoLab Blog · 국내주식 데이터 실무 · 2026

국내 주식 전체 종목코드 받기 — kospi_code.mst 파싱

데이터 · 종목 유니버스 2026-08-08 · 약 9분 읽기 · 알고랩 AlgoLab
한 줄 요약 코스피·코스닥 전 종목 코드는 API 조회로 받는 게 아니라, 압축된 마스터 파일을 내려받아 직접 파싱합니다. 한국투자증권이 배포하는 kospi_code.mst.zip·kosdaq_code.mst.zip을 받아 압축을 풀면 한 줄이 한 종목인 텍스트 파일이 나옵니다. 여기서 막히는 지점은 늘 셋입니다 — ① 인코딩이 cp949(그냥 열면 UnicodeDecodeError), ② 한 줄이 "앞쪽 가변 + 뒤쪽 고정폭" 구조split()이 아니라 자리수로 잘라야 하고 그 고정폭 길이가 코스피 228자·코스닥 222자로 다름, ③ 코드가 두 종류(주문에 넣는 단축코드 005930 vs 표준코드 KR7005930003). 이 셋만 넘기면 나머지는 pandas 한 줄입니다.

자동매매 봇을 만들 때 의외로 첫날 막히는 지점이 "살 종목 후보 목록을 어디서 받나"입니다. KIS API 키를 발급받고 첫 주문까지 성공했더라도, 그 주문에는 종목코드를 이미 알고 있다는 전제가 깔려 있습니다. 삼성전자 하나를 사는 봇이면 005930을 하드코딩하면 그만이지만, "코스피 전 종목 중 조건에 맞는 것"을 찾는 순간 목록 자체가 필요해집니다.

그런데 증권사 REST API의 조회 계열은 대부분 종목코드를 입력으로 받는 구조라 전 종목을 훑어 주지 않습니다. 그래서 종목 목록은 API가 아니라 파일로 배포됩니다.

목차

  1. 파일은 어디에 있나 — 내려받기와 압축 해제
  2. 한 줄이 어떻게 생겼나 — 가변 + 고정폭 구조
  3. 파싱 코드 전문 (코스피·코스닥)
  4. 함정 3가지 — cp949 · 자리수 · 코드 두 종류
  5. 목록을 그대로 쓰면 안 되는 이유 — 걸러야 할 것들
  6. pykrx·FinanceDataReader와 무엇이 다른가
  7. 봇에 붙이는 구조 — 하루 한 번 갱신 캐시

1. 파일은 어디에 있나 — 내려받기와 압축 해제

한국투자증권은 종목정보 마스터 파일을 공개 다운로드 서버에 .zip으로 올려 둡니다. 개발자센터의 인증(appkey·appsecret·access_token)이 필요 없고, 그냥 HTTP로 받으면 됩니다. 호출 제한과도 무관합니다.

# 코스피
https://new.real.download.dws.co.kr/common/master/kospi_code.mst.zip
# 코스닥
https://new.real.download.dws.co.kr/common/master/kosdaq_code.mst.zip

파이썬으로 받아서 푸는 것까지는 열 줄이 안 됩니다.

import ssl, urllib.request, zipfile, os

BASE = "https://new.real.download.dws.co.kr/common/master/"

def download_mst(market: str, out_dir: str = ".") -> str:
    """market: 'kospi' | 'kosdaq'  ->  압축 푼 .mst 파일 경로"""
    name = f"{market}_code.mst"
    zip_path = os.path.join(out_dir, name + ".zip")

    # 일부 환경에서 인증서 검증 옵션 때문에 막히는 경우가 있어 컨텍스트를 명시
    ctx = ssl.create_default_context()
    with urllib.request.urlopen(BASE + name + ".zip", context=ctx) as r, \
         open(zip_path, "wb") as f:
        f.write(r.read())

    with zipfile.ZipFile(zip_path, "r") as z:
        z.extractall(out_dir)
    os.remove(zip_path)
    return os.path.join(out_dir, name)

path = download_mst("kospi")
print(path, os.path.getsize(path), "bytes")

확장자가 .mst라고 특수 포맷이 아닙니다. 그냥 텍스트 파일이라 .txt로 이름만 바꿔도 메모장에서 열립니다. 다만 메모장으로 열면 한글이 깨져 보일 수 있는데, 파일이 잘못된 게 아니라 인코딩 문제입니다(4장).

2. 한 줄이 어떻게 생겼나 — 가변 + 고정폭 구조

이 파일이 초보자를 잡는 이유는 CSV가 아니기 때문입니다. 쉼표도 탭도 없습니다. 한 줄이 한 종목인데, 그 줄이 두 구간으로 나뉩니다.

한 줄 = 한 종목 단축코드 [0:9] 표준코드 [9:21] 한글 종목명 [21:] — 길이가 종목마다 다름 고정폭 60여 개 필드 그룹코드 · 시가총액규모 · 기준가 · 상장일자 … 앞쪽 = 가변 구간 (자리수로 자름) 뒤쪽 = 고정폭 구간 코스피 228자 · 코스닥 222자 ← 시장마다 다름 앞쪽 = row[0 : len(row) - N] 뒤쪽 = row[len(row) - N : ] (N = 228 또는 222)
마스터 파일 레코드 구조 — 뒤에서부터 N자를 떼어내야 앞쪽이 결정된다

핵심은 "뒤에서부터 자른다"는 점입니다. 종목명 길이가 제각각이라 앞에서부터는 끝을 알 수 없지만, 뒤쪽 고정폭 구간의 길이는 항상 같기 때문에 뒤에서 228자(코스닥은 222자)를 떼어 내면 남는 것이 앞쪽 구간이 됩니다. 그러고 나면 앞쪽은 자리수가 정해져 있습니다.

구간자리필드예시
[0:9]단축코드005930    (공백 패딩)
[9:21]표준코드(ISIN)KR7005930003
[21:]한글 종목명삼성전자
고정폭그룹코드·시가총액규모·기준가·상장일자 등 60여 개시장별 레이아웃 문서 참조

절대 하면 안 되는 것 — row.split(). 종목명에 공백이 들어간 종목이 흔합니다(예: 지주회사·스팩·리츠 이름). 공백으로 자르는 순간 그런 종목만 필드가 밀려서, 대부분은 멀쩡한데 몇십 개만 조용히 틀리는 최악의 버그가 됩니다. 봇에서는 이게 "없는 종목코드로 주문 시도"로 이어집니다.

3. 파싱 코드 전문 (코스피·코스닥)

앞쪽 세 필드만 있으면 종목 유니버스는 완성됩니다. 뒤쪽 60여 개 필드는 필요할 때 pandasread_fwf로 붙이면 되고, 대부분의 자동매매 봇은 코드·이름·시장 구분 세 개면 충분합니다.

import pandas as pd

TAIL_LEN = {"kospi": 228, "kosdaq": 222}   # ← 시장마다 다르다

def parse_mst(path: str, market: str) -> pd.DataFrame:
    n = TAIL_LEN[market]
    rows = []
    # 인코딩 고정: cp949 (UTF-8로 열면 UnicodeDecodeError)
    with open(path, mode="r", encoding="cp949") as f:
        for line in f:
            row = line.rstrip("\n")
            if len(row) <= n:          # 빈 줄·꼬리 방어
                continue
            head = row[0:len(row) - n]  # 앞쪽 가변 구간
            rows.append({
                "code"  : head[0:9].strip(),    # 단축코드 → 공백 제거 필수
                "isin"  : head[9:21].strip(),   # 표준코드
                "name"  : head[21:].strip(),    # 한글 종목명
                "market": market.upper(),
            })
    return pd.DataFrame(rows)

kospi  = parse_mst(download_mst("kospi"),  "kospi")
kosdaq = parse_mst(download_mst("kosdaq"), "kosdaq")
universe = pd.concat([kospi, kosdaq], ignore_index=True)

print(len(universe))
print(universe[universe["name"] == "삼성전자"])

결과는 이렇게 나옵니다.

       code          isin    name market
1234  005930  KR7005930003  삼성전자  KOSPI

파싱이 맞았는지 30초 만에 확인하는 법. universe["code"].str.len().value_counts()를 찍어 보십시오. 전부 6이 나와야 정상입니다. 5나 7이 섞여 있으면 자리수를 잘못 잘랐거나 strip()을 빠뜨린 것입니다. 이어서 universe["name"].str.contains(r"\d{4}").sum()이 크게 나오면 뒤쪽 고정폭 길이(TAIL_LEN)를 잘못 준 것입니다 — 숫자 필드가 종목명 쪽으로 밀려 들어온 상태입니다.

4. 함정 3가지 — cp949 · 자리수 · 코드 두 종류

함정 ① 인코딩이 cp949다

open(path)를 그냥 부르면 파이썬은 환경 기본 인코딩(요즘은 대개 UTF-8)으로 읽으려 하고, 곧바로 이런 예외가 납니다.

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbb in position 21:
invalid start byte

encoding="cp949"를 명시하면 해결됩니다. 같은 함정이 저장할 때도 반복됩니다 — 파싱 결과를 to_csv()로 저장한 뒤 엑셀에서 열면 한글이 깨집니다. 저장은 encoding="utf-8-sig"로 하면 엑셀과 파이썬 양쪽에서 안전합니다.

universe.to_csv("universe.csv", index=False, encoding="utf-8-sig")

함정 ② 코스피 코드를 코스닥에 그대로 쓴다

가장 흔한 실수입니다. 코스피용으로 만든 파싱 함수를 그대로 코스닥에 돌리면 228과 222의 차이인 6자만큼 모든 필드가 밀립니다. 그런데 예외가 나지 않습니다. 종목명 끝에 숫자 몇 개가 붙거나 코드 앞이 잘리는 식으로 조용히 틀리기 때문에, 며칠 뒤 "왜 이 종목만 주문이 거부되지?"로 발견됩니다.

그래서 위 코드에서는 길이를 TAIL_LEN 딕셔너리 상수로 분리했습니다. 숫자를 함수 안에 박아 두면 언젠가 반드시 복사·붙여넣기 사고가 납니다. 주문 단계에서 나는 에러의 상당수가 사실은 이런 데이터 준비 단계의 오염에서 시작됩니다.

함정 ③ 코드가 두 종류다 — 단축코드 vs 표준코드

마스터 파일에는 코드가 두 개 들어 있습니다.

단축코드표준코드(ISIN)
모양005930 (6자리)KR7005930003 (12자리)
쓰는 곳주문·시세 조회 파라미터, HTS 검색창국제 표준 식별, 대외 보고
파일에서head[0:9]공백 패딩 있음head[9:21]
주의strip() 안 하면 "005930 "이 그대로 요청에 실림이걸 주문에 넣으면 종목 없음 처리

증권사 REST API에 실제로 넣는 것은 여섯 자리 단축코드입니다. 필드명은 증권사마다 다르므로(한국투자증권과 키움 REST API의 파라미터 이름이 서로 다릅니다) 정확한 이름은 각사 공식 개발자 문서에서 확인하십시오. 공백이 붙은 채로 보내면 대개 "해당 종목 없음" 계열 응답이 돌아오는데, 눈으로는 코드가 맞아 보여서 원인을 찾는 데 시간이 오래 걸립니다.

5. 목록을 그대로 쓰면 안 되는 이유 — 걸러야 할 것들

파싱에 성공하면 수천 줄짜리 표가 생깁니다. 그런데 여기 있는 게 전부 "우리가 생각하는 주식"은 아닙니다. 마스터 파일에는 보통주 외에도 ETF·리츠·스팩·우선주·투자회사 등이 함께 들어 있습니다. 전략이 "코스피 전 종목 모멘텀"인데 유니버스에 ETF가 섞여 있으면, 백테스트 결과는 사실상 다른 전략의 결과가 됩니다.

구분에 쓰는 것이 뒤쪽 고정폭 구간의 그룹코드 필드입니다. 다만 코드 값을 여기서 외우려 하지 마십시오 — 값 체계는 시장·시기에 따라 달라질 수 있습니다. 대신 직접 세어 보는 것이 가장 확실합니다.

# 뒤쪽 고정폭 구간에서 앞 2자리(그룹코드)만 떼어 분포를 본다
def group_code(path: str, market: str) -> pd.Series:
    n = TAIL_LEN[market]
    codes = []
    with open(path, encoding="cp949") as f:
        for line in f:
            row = line.rstrip("\n")
            if len(row) > n:
                codes.append(row[len(row) - n : len(row) - n + 2])
    return pd.Series(codes).value_counts()

print(group_code("kospi_code.mst", "kospi"))
# 값별 건수를 보고, 대표 종목 몇 개를 눈으로 확인해
# "이 값이 보통주" "이 값이 ETF"를 직접 대응시킨다

우선주 걸러내기는 코드 규칙으로도 가능합니다. 관례상 보통주는 여섯 자리가 0으로 끝나고(005930), 우선주는 그 자리가 5·7 등으로 바뀝니다(005935). 다만 모든 종목에 예외 없이 적용되는 규칙으로 단정하지는 마십시오. 실무에서는 코드 규칙 + 종목명에 "우"가 붙는지를 함께 보고, 최종적으로는 추려 낸 목록을 한 번 눈으로 훑는 검수를 넣는 편이 안전합니다. 상장 종목 분류의 공식 기준은 한국거래소와 각 증권사 공식 자료에서 확인하시기 바랍니다.

그리고 이 파일은 고정된 목록이 아닙니다. 신규 상장·상장폐지·종목명 변경이 계속 반영됩니다. 한 번 받아 두고 몇 달 쓰면 이미 없는 종목에 주문을 넣거나, 새로 상장한 종목을 영원히 못 보게 됩니다. 종목을 이름이 아니라 코드로 매칭하는 것도 같은 이유입니다 — 이름은 바뀝니다.

🛠️
데이터 준비까지가 봇 개발의 절반입니다

전략 로직은 몇십 줄인데, 종목 유니버스·갱신 스케줄·검증까지 붙이면 일이 몇 배로 늘어납니다. 어디까지 직접 하고 어디부터 맡길지 고민 중이라면 편하게 물어보세요.

자동매매 제작 상담하기 →

6. pykrx·FinanceDataReader와 무엇이 다른가

"이걸 왜 직접 파싱하나, pykrx 쓰면 한 줄인데"라고 생각하실 수 있습니다. 맞습니다. 목적이 다를 뿐입니다.

구분마스터 파일 (.mst)pykrx / FinanceDataReader
출처주문을 넣을 증권사가 직접 배포외부 사이트를 읽어 오는 래퍼
설치불필요 (표준 라이브러리 + pandas)pip install 필요
코드량20줄 남짓1~2줄
깨질 때파일 레이아웃이 바뀔 때 (드묾)원본 사이트 응답이 바뀔 때
부가 정보그룹코드·시가총액규모·기준가 등 60여 필드 동봉라이브러리가 제공하는 범위 내
적합실거래 봇의 기준 유니버스백테스트·리서치·빠른 프로토타입

실무 권장은 둘 다 쓰되 역할을 나누는 것입니다. 주문을 보낼 증권사가 배포한 마스터 파일을 "정본"으로 삼고, pykrx·FinanceDataReader보조·교차검증에 씁니다. 두 목록의 종목 수가 크게 어긋나면 어느 한쪽이 깨진 것이므로, 그 자체가 좋은 알람이 됩니다. 가격 데이터 쪽 선택은 파이썬 주식 데이터 소스 비교에서 따로 다뤘습니다.

7. 봇에 붙이는 구조 — 하루 한 번 갱신 캐시

실제 봇에서는 전략이 돌 때마다 파일을 받으면 안 됩니다. 파일이 수 MB이고 내용은 하루 단위로만 바뀌므로, 장 시작 전 1회 갱신 → 로컬 캐시 → 전략은 캐시만 조회가 정석입니다.

import datetime as dt, pathlib

CACHE = pathlib.Path("cache")

def load_universe(market: str) -> pd.DataFrame:
    CACHE.mkdir(exist_ok=True)
    today = dt.date.today().isoformat()
    cached = CACHE / f"{market}_{today}.parquet"

    if cached.exists():                       # 오늘 것이 있으면 재사용
        return pd.read_parquet(cached)

    df = parse_mst(download_mst(market, str(CACHE)), market)

    # ── 최소 검증: 통과 못 하면 어제 캐시를 계속 쓴다 ──
    assert df["code"].str.len().eq(6).all(),  "단축코드 자리수 이상"
    assert len(df) > 500,                     "종목 수가 비정상적으로 적다"

    df.to_parquet(cached)
    return df

검증에 실패했을 때 "빈 목록으로 진행"하지 마십시오. 다운로드 서버가 잠깐 응답하지 않거나 레이아웃이 바뀌면 파싱 결과가 0건이 될 수 있습니다. 이때 봇이 그냥 진행하면 "조건에 맞는 종목이 하나도 없다"로 해석해 아무 일도 안 하거나, 반대로 기존 보유분을 전량 청산하는 리밸런싱을 실행할 수도 있습니다. 실패는 실패로 처리하고 직전 캐시를 유지한 뒤 사람에게 알리는 것이 맞습니다. 장애 복구 플레이북의 기본 원칙과 같습니다.

조건에 맞는 종목을 증권사 쪽에서 골라 주는 방법도 있습니다. 키움은 HTS에서 만든 검색식을 API로 불러와 편입 종목을 실시간으로 받을 수 있는데, 이 경우 유니버스를 직접 관리할 필요가 줄어듭니다. 자세한 절차는 키움 REST API 조건검색에 정리해 두었습니다.

자주 묻는 질문

KIS API에 전체 종목코드를 내려주는 엔드포인트가 있나요?

한국투자증권 KIS REST API의 조회 계열은 대부분 종목코드를 입력으로 받는 구조라, 전 종목을 훑어 주는 용도가 아닙니다. 그래서 전 종목 목록은 마스터 파일로 받습니다. 배포 주소와 파일 레이아웃은 공식 개발자센터와 공식 깃허브 저장소의 샘플 코드를 기준으로 확인하시기 바랍니다.

kospi_code.mst 파일을 열었더니 한글이 깨집니다.

인코딩이 cp949이기 때문입니다. open(path, encoding="cp949")로 읽으십시오. 저장할 때는 utf-8-sig를 쓰면 엑셀에서도 깨지지 않습니다.

단축코드와 표준코드 중 주문 API에는 무엇을 넣나요?

여섯 자리 단축코드(005930)입니다. 표준코드(KR7005930003)는 함께 들어 있지만 주문 파라미터로는 쓰지 않는 것이 일반적입니다. 파일에서 읽은 뒤 strip()으로 공백을 제거하는 것을 잊지 마십시오.

코스피 파싱 코드를 코스닥에 그대로 쓰면 되나요?

안 됩니다. 뒤쪽 고정폭 길이가 코스피 228자, 코스닥 222자로 다릅니다. 예외 없이 조용히 값이 밀리므로, 길이를 시장별 상수로 분리하고 파싱 직후 검증을 넣으십시오.

pykrx를 쓰면 마스터 파일은 안 써도 되나요?

리서치·백테스트라면 pykrx가 훨씬 간편합니다. 다만 실제 주문을 넣는 봇이라면 주문을 받을 증권사가 배포하는 목록을 기준으로 삼고, 라이브러리는 교차검증용으로 두는 조합을 권합니다.

확인 캐치. 이 글의 다운로드 주소·인코딩·자리수는 2026년 8월 8일 기준 한국투자증권 공식 깃허브 저장소(open-trading-api)의 종목정보 샘플 코드를 기준으로 정리했습니다. 파일 레이아웃과 배포 경로는 증권사 사정으로 변경될 수 있으므로, 실제 구현 전 공식 개발자센터·공식 저장소에서 최신 값을 확인하시기 바랍니다. 종목 분류(보통주·우선주·ETF 등)의 공식 기준은 한국거래소 자료를 따르십시오.

마무리

정리하면 세 줄입니다. 전 종목 코드는 API가 아니라 kospi_code.mst.zip·kosdaq_code.mst.zip 파일로 받는다. 인코딩은 cp949, 파싱은 뒤에서 228자(코스닥 222자)를 떼고 앞쪽을 자리수로 자른다. 주문에는 공백을 지운 여섯 자리 단축코드를 넣는다.

나머지는 운영의 문제입니다 — 언제 갱신할지, 무엇을 걸러 낼지, 실패했을 때 어떻게 멈출지. 종목 목록 하나가 틀어지면 그 위에 올린 전략·주문이 전부 함께 틀어집니다.

종목 유니버스부터 주문까지, 통째로 맡기고 싶다면

데이터 수집·갱신·검증까지 포함해 설계합니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기