6 분 분량

입문 시리즈는 미국 공개 데이터와 코스피 지수로 돌렸습니다. 실전 시리즈는 한국 종목으로 갑니다. 열 편 전체의 목차와 한 줄 요약은 퀀트 실전, 10편 정리에 있습니다. 첫 관문은 데이터입니다. 종목별 PER, PBR, 배당수익률, 시가총액을 특정 날짜 기준으로, 전 종목 한 번에 받아와야 스크리닝이든 팩터 백테스트든 시작할 수 있습니다.

이 일에 가장 많이 쓰는 도구가 pykrx입니다. 한국거래소(KRX) 정보데이터시스템에서 내려주는 값을 pandas 표로 받아주는 라이브러리입니다. 가격은 FinanceDataReader로도 받을 수 있지만, 전 종목 재무 지표를 날짜 단위로 주는 건 이쪽입니다.

먼저 걸리는 것: 로그인

예전 글들을 보고 pip install pykrx 하고 바로 호출하면 이런 메시지가 뜹니다.

KRX 로그인 실패: KRX_ID 또는 KRX_PW 환경 변수가 설정되지 않았습니다.

KRX가 데이터 포털에 로그인을 요구하도록 바뀌었고, pykrx 1.2 이후 버전은 그에 맞춰 계정으로 로그인한 세션을 씁니다. 라이브러리 안을 열어보면 KRX_ID, KRX_PW 환경 변수를 읽어 로그인하고, 세션이 만료되면 자동으로 다시 로그인하게 되어 있습니다. 계정은 KRX 정보데이터시스템에서 무료로 만들 수 있습니다.

# macOS / Linux
export KRX_ID="아이디"
export KRX_PW="비밀번호"

# Windows PowerShell
$env:KRX_ID = "아이디"
$env:KRX_PW = "비밀번호"

비밀번호를 코드나 노트북 안에 직접 쓰지 마세요. 환경 변수로 두면 코드를 공개해도 계정 정보는 드러나지 않습니다.

함수 네 개면 됩니다

pykrx 함수는 수십 개지만, 재무 스냅샷을 만드는 데 필요한 건 넷입니다.

함수 주는 것 인자
get_market_ticker_list(date, market) 그 날짜의 상장 종목 코드 목록 market은 KOSPI / KOSDAQ / KONEX / ALL
get_market_fundamental(date, market=...) 전 종목 BPS, PER, PBR, EPS, DIV, DPS 날짜 하나를 주면 전 종목, 두 개를 주면 한 종목의 기간
get_market_cap_by_ticker(date, market=...) 전 종목 종가, 시가총액, 거래량, 상장주식수  
get_market_ohlcv(fromdate, todate, ticker) 한 종목의 일별 OHLCV adjusted=True가 기본(수정주가)

날짜는 전부 "20260630" 형식의 문자열입니다. -가 들어가도 알아서 지워주지만, 처음부터 여덟 자리로 쓰는 습관이 낫습니다. 종목 코드도 문자열입니다. 5930이 아니라 "005930"입니다. 정수로 들고 있으면 앞의 0이 사라져 없는 코드가 됩니다. 요즘은 00104K, 0099X0처럼 영문이 섞인 코드도 있어서(2026년 9월 기준 80개) 애초에 숫자로 바꿀 수도 없습니다.

get_market_fundamental은 인자 개수로 동작이 갈립니다. 날짜 하나면 “그날 전 종목”, 날짜 둘과 종목 코드면 “그 종목의 기간별”입니다. 같은 이름으로 두 가지 일을 하니 처음에 헷갈립니다.

하루치 스냅샷 만들기

펀더멘털 표와 시가총액 표를 종목 코드로 합치고, 종목명을 붙이고, CSV로 저장합니다.

import time
import pandas as pd
from pykrx import stock

def fetch_snapshot(date: str, market: str = "KOSPI") -> pd.DataFrame:
    """특정 일자의 전 종목 펀더멘털 + 시가총액을 한 표로 합친다."""
    # 휴일이면 직전 영업일로 대체 (alternative=True)
    fund = stock.get_market_fundamental_by_ticker(date, market=market, alternative=True)
    time.sleep(1)                                   # 연속 호출 사이에 쉬어 준다
    cap = stock.get_market_cap_by_ticker(date, market=market, alternative=True)
    time.sleep(1)
    names = pd.Series({t: stock.get_market_ticker_name(t) for t in fund.index}, name="종목명")

    df = fund.join(cap[["종가", "시가총액", "상장주식수"]], how="inner").join(names)
    df.index.name = "티커"
    return df

df = fetch_snapshot("20260630")
df.to_csv("krx_snapshot_20260630.csv", encoding="utf-8-sig")

alternative=True는 휴일을 넣었을 때 직전 영업일 값을 돌려달라는 뜻입니다. 이게 없으면 빈 표가 옵니다. time.sleep(1)은 예의입니다. 전 종목을 한 번에 받는 호출은 서버 쪽 부담이 크고, 연속으로 두드리면 차단당합니다.

한 날짜에 세 번 호출로 끝나므로, 월말 기준 5년치 스냅샷을 만들어도 180번 정도입니다. 결과는 반드시 파일로 저장하세요. 같은 데이터를 다시 받는 것이 가장 흔한 낭비입니다.

받은 다음 반드시 손볼 것

여기가 이 글에서 제일 중요한 부분입니다. KRX가 주는 표에는 “값이 없음”을 0으로 적는 관례가 있습니다.

열 0의 뜻 그대로 쓰면
PER 적자 종목 (EPS ≤ 0) “PER 낮은 순”으로 정렬하면 적자 기업이 맨 위에 옵니다
BPS, PBR 값 없음 PBR 0.0이 “극단적으로 싼 주식”으로 잡힙니다
DIV, DPS 무배당 이건 실제로 0이 맞습니다

저PER 스크리닝을 하면서 이걸 모르면 적자 기업 목록을 뽑아놓고 “싼 주식”이라 부르게 됩니다. 실제 값과 없음을 구분해서 NaN으로 바꿔야 합니다.

import numpy as np

def clean(df: pd.DataFrame) -> pd.DataFrame:
    """KRX 의 '0 = 없음' 관례를 NaN 으로 바꾸고, 스크리닝에 쓸 파생 열을 만든다."""
    out = df.copy()
    out.loc[out["EPS"] <= 0, "PER"] = np.nan      # 적자: PER 0 은 값이 아니라 표시
    out.loc[out["BPS"] <= 0, ["BPS", "PBR"]] = np.nan
    out.loc[out["DPS"] <= 0, "DIV"] = 0.0
    out["시총(억)"] = (out["시가총액"] / 1e8).round(0)
    out["이익수익률"] = (out["EPS"] / out["종가"]).where(out["EPS"] > 0)   # 1/PER
    return out

마지막 줄의 이익수익률은 PER의 역수입니다. 다음 글에서 종목을 줄 세울 때 PER 대신 이걸 씁니다. 정렬 방향이 “클수록 싸다”로 직관과 맞고, 적자 종목은 결측으로 남겨 순위 맨 아래로 보내기 쉽습니다. (KRX는 적자 종목의 EPS 자체를 0으로 주기 때문에 손실의 크기는 알 수 없습니다. 아래 실제 표에서 확인합니다.)

시점 문제

get_market_fundamental("20260630")이 주는 EPS와 BPS는 그 시점에 KRX가 적용하고 있던 직전 확정 실적입니다. 6월 30일에 조회하면 대개 전년도 사업보고서 기준입니다. 그래서 “2025년 실적으로 2025년 1월의 PER을 계산”하는 실수는 이 함수로는 생기지 않습니다. 그날 실제로 공개되어 있던 숫자를 주기 때문입니다.

이건 장점이면서 주의점입니다. 장점은 미래 참조 편향이 자동으로 막힌다는 것이고, 주의점은 KRX가 대부분 종목의 재무 지표를 5월에 한꺼번에 갱신해서, 사업보고서가 이미 나온 4월에도 PER이 전년도가 아니라 전전년도 실적 기준이라는 것입니다. 정밀하게 하려면 사업보고서 공시일을 따로 붙여야 합니다. 이 시리즈에서는 공시일을 붙이지 않았으므로, 사업보고서가 나온 뒤 KRX가 갱신하기 전까지는 실적이 한 해 늦게 반영된다는 점을 한계로 남겨 둡니다.

실제로 받아 보면

(2026-09-21 업데이트: 2026년 9월 18일 KRX 스냅샷 결과로 이 절을 채웠습니다.)

2026년 9월 18일 기준으로 market="ALL"로 받으면 2,714종목이 옵니다(코스피 914, 코스닥 1,800). 정제하기 전 원본에서 코스피 종목만 추린 앞부분입니다.

티커 종목명 BPS PER PBR EPS DIV DPS 종가
095570 AJ네트웍스 10,216 6.47 0.40 635 8.03 330 4,110
006840 AK홀딩스 31,492 0.00 0.21 0 0.00 0 6,750
027410 BGF 18,674 6.04 0.20 618 3.48 130 3,735
282330 BGF리테일 76,009 12.24 1.82 11,303 2.96 4,100 138,400
138930 BNK금융지주 34,780 6.21 0.45 2,512 4.71 735 15,600

두 번째 줄이 위에서 말한 관례 그대로입니다. AK홀딩스는 PER 0.00, EPS 0입니다. PER이 0인 게 아니라 적자라는 표시입니다. 전체로 세어 보면 이렇습니다.

확인한 것 결과
EPS가 0인 종목 1,172개 (43%). EPS가 음수인 종목은 0개
PER이 0인 종목 1,172개. EPS 0인 종목과 정확히 일치
BPS가 0인 종목 200개. 그중 우선주 114개, 스팩 69개
DPS가 0인 종목 1,380개. DPS는 0인데 DIV가 0이 아닌 경우 0개

적자 기업의 EPS를 0으로 주는 것, BPS가 없는 종목이 대부분 우선주와 스팩이라는 것, 배당 0은 실제 0이라는 것. 앞 절의 표가 실제 데이터에서 그대로 확인됩니다.

정제 전후로 “PER 낮은 순” 상위 5개가 어떻게 달라지는지도 봅니다.

  정제 전 (0 포함) 정제 후
1 블루산업개발 0.00 한창 0.40
2 전진바이오팜 0.00 예림당 0.51
3 삼성전기우 0.00 효성화학 0.66
4 삼성에피스홀딩스 0.00 에스코넥 0.79
5 삼성스팩13호 0.00 베셀 0.83

정제 전에는 적자 기업, 우선주, 스팩이 “가장 싼 주식” 자리를 차지합니다. 정제 후 목록도 그대로 믿을 건 아닙니다. PER 0.4는 1년에 주가의 2.5배를 벌었다는 뜻인데, 대개 일회성 이익(자산 매각, 지분 평가이익)이 EPS에 들어간 경우입니다. 극단값은 항상 이유가 있습니다. 이건 다음 글의 스크리닝에서 다룹니다.

시가총액 상위 5개입니다. 정제 후 표라 우선주의 PER·PBR은 결측으로 나옵니다.

종목명 종가 시총(조) PER PBR 배당수익률
삼성전자 261,000 1,526 39.5 4.08 0.64%
SK하이닉스 1,857,000 1,357 29.9 10.81 0.16%
삼성전자우 196,100 157 결측 결측 0.85%
SK스퀘어 1,079,000 142 16.2 5.15 0.00%
삼성전기 1,388,000 104 148.5 10.99 0.17%

KRX는 우선주에 재무 지표를 붙이지 않습니다. 삼성전자우의 PER이 정제 전에는 0.00이었고, 정제 후에는 결측입니다. 우선주를 스크리닝에 넣을 생각이면 보통주의 지표를 따로 붙여야 합니다.

위의 받기와 정제를 묶은 전체 코드는 krx_snapshot.py입니다. 세 가지 방식으로 돌아갑니다. 날짜를 주면 pykrx로 받고, --file로 받아 둔 원본을 주면 정제만 하고, --synthetic을 주면 네트워크 없이 난수 표로 정제 코드만 시험합니다. 이 글의 표는 9월 18일 원본 스냅샷을 --file로 넣어 만들었습니다.

워크포워드와 몬테카를로 글은 2024년 12월부터 2026년 8월까지 월말 스냅샷 21개와 9월 18일 스냅샷으로 돌립니다. 이 파일들은 krx_fetch_monthly.py로 받았습니다. KRX 데이터 파일은 이 사이트에서 다시 배포하지 않으므로, 같은 결과를 보려면 KRX 계정으로 직접 받아야 합니다. 스크립트 목록은 코드 페이지에 있습니다.

다음 글에서는 이 스냅샷 CSV를 재료로 종목 스크리닝을 만듭니다. 이익수익률, PBR, 배당수익률로 줄을 세우고, 시가총액 하한을 두고, 결과가 왜 지주회사와 금융주 쪽으로 쏠리는지 봅니다.

2026-09-28 수정: PER 0.4를 “주가의 40%를 1년에 벌었다”고 풀이한 부분을 “1년에 주가의 2.5배를 벌었다”로, 재무 지표 갱신 시점 설명을 “KRX가 대부분 종목을 5월에 한꺼번에 갱신해 4월에도 전전년도 실적이 쓰인다”로, 받아 둔 스냅샷 구성을 “월말 21개와 9월 18일 1개”로 바로잡았습니다.


이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.

최종 수정: