파이썬 주식 데이터 수집 — pykrx·FinanceDataReader·KIS API 비교
pykrx, 국내·해외를 한 인터페이스로 다루고 종목 리스트가 필요하면 FinanceDataReader, 실거래 봇과 같은 값을 써야 하면 한국투자증권 KIS API(FHKST03010100)입니다. 그리고 어느 쪽을 쓰든 수정주가·생존편향·티커 변경·호출 제한 네 가지에서 데이터가 조용히 틀어집니다.
자동매매를 만들기로 마음먹고 파이썬을 열면, 대부분 전략을 짜기 훨씬 전에 여기서 한 번 멈춥니다. "백테스트를 돌리려면 과거 주가가 있어야 하는데, 그 데이터는 대체 어디서 받나?"
검색하면 pykrx, FinanceDataReader, yfinance, 증권사 API 같은 이름이 쏟아지는데 뭐가 뭘 해주는지 정리된 곳이 잘 없습니다. 이 글은 그 셋을 실제 코드와 응답으로 비교하고, 더 중요한 것 — 받아온 데이터가 조용히 틀려 있는 네 가지 경우를 짚습니다. 백테스트 결과가 이상한데 코드에는 문제가 없다면, 원인은 대개 전략이 아니라 데이터입니다.
이 글의 순서
- 데이터 소스 3갈래 지도
- pykrx — KRX 고유 항목이 필요할 때
- FinanceDataReader — 국내·해외를 한 번에
- KIS REST API — 실거래와 같은 값을 쓸 때
- 데이터가 조용히 틀어지는 함정 4가지
- 한눈 비교표와 실전 조합
- 두 소스 교차검증 코드
- 자주 묻는 질문
1. 데이터 소스 3갈래 지도
국내주식 데이터를 파이썬으로 가져오는 방법은 사실상 세 갈래입니다. 셋 중 하나를 고르는 게 아니라, 용도에 따라 나눠 쓰는 것이 정답에 가깝습니다.
| 갈래 | 대표 | 잘하는 것 | 한계 |
|---|---|---|---|
| 무료 라이브러리 (KRX·포털 기반) | pykrx |
시가총액·투자자별 매매동향·펀더멘털 등 KRX 고유 항목 | 국내 전용, 조회 속도가 느릴 수 있음 |
| 무료 라이브러리 (통합형) | FinanceDataReader |
국내·해외·환율·지수를 같은 문법으로, 종목 리스트 일괄 조회 | 소스에 따라 수정주가 처리가 갈림 |
| 증권사 API | 한국투자증권 KIS API | 실거래 봇이 쓰는 것과 같은 출처의 값 | 계좌·appkey 필요, 호출 제한·행 수 제한 |
기준은 단순합니다. 백테스트 단계에서는 무료 라이브러리로 빠르게 돌리고, 실거래에 올릴 때는 증권사 API 값으로 맞춰본다. 백테스트를 어떤 도구로 돌릴지 자체가 아직 안 정해졌다면 백테스팅 프로그램·사이트 5갈래 비교를 먼저 보시는 편이 좋습니다.
2. pykrx — KRX 고유 항목이 필요할 때
pykrx는 한국거래소(KRX)와 포털의 공개 데이터를 파이썬에서 pandas DataFrame으로 받아오는 라이브러리입니다. OHLCV뿐 아니라 시가총액, 투자자별 순매수, 등락률 순위 같은 국내 특유의 항목을 다룰 수 있다는 점이 강점입니다.
pip install pykrx
from pykrx import stock
# 삼성전자 일봉 (수정주가 여부는 adjusted 인자로 제어)
df = stock.get_market_ohlcv("20250101", "20251231", "005930")
print(df.tail(3))
# 시가 고가 저가 종가 거래량
# 날짜
# 2025-12-27 71800 72400 71500 72100 11238450
# 2025-12-28 72100 72900 71900 72800 9874210
# 2025-12-29 72800 73100 72200 72300 10553120
# 특정일 전 종목 시가총액 — 유니버스를 만들 때 쓴다
cap = stock.get_market_cap("20251229", market="KOSPI")
print(cap.columns.tolist())
# ['종가', '시가총액', '거래량', '거래대금', '상장주식수']
# 투자자별 순매수 (외국인·기관 수급을 팩터로 쓸 때)
flow = stock.get_market_trading_value_by_date("20250101", "20251231", "005930")
컬럼명이 한글이라는 점을 기억하세요. backtrader·vectorbt 같은 백테스트 라이브러리는 open/high/low/close/volume을 기대하므로 rename이 필요합니다. 이 사소한 차이 때문에 첫 실행이 실패하는 경우가 많습니다.
df = df.rename(columns={"시가":"open","고가":"high","저가":"low",
"종가":"close","거래량":"volume"})
df.index.name = "date"
3. FinanceDataReader — 국내·해외를 한 번에
FinanceDataReader(줄여서 FDR)는 국내주식·미국주식·환율·지수·암호화폐를 같은 함수 하나로 부를 수 있게 만든 라이브러리입니다. 국내와 해외를 함께 보는 전략이라면 코드가 훨씬 단순해집니다.
pip install finance-datareader
import FinanceDataReader as fdr
kr = fdr.DataReader("005930", "2025-01-01", "2025-12-31") # 삼성전자
us = fdr.DataReader("AAPL", "2025-01-01", "2025-12-31") # 애플
fx = fdr.DataReader("USD/KRW","2025-01-01", "2025-12-31") # 환율
print(kr.tail(2))
# Open High Low Close Volume Change
# Date
# 2025-12-28 72100 72900 71900 72800 9874210 0.009709
# 2025-12-29 72800 73100 72200 72300 10553120 -0.006868
# 종목 리스트 통째로 — 유니버스 구성의 출발점
krx = fdr.StockListing("KRX")
print(krx.shape, krx.columns.tolist()[:6])
StockListing("KRX")로 상장 종목을 한 번에 받을 수 있다는 점이 큰 장점입니다. 다만 이 리스트는 "오늘 살아 있는 종목"이라는 사실이 뒤에서 다룰 생존편향의 출발점이 됩니다.
둘 중 하나만 골라야 하나? 아닙니다. 실무에서는 가격은 FDR, 시가총액·수급은 pykrx처럼 나눠 쓰고, 겹치는 구간은 값이 같은지 대조합니다. 둘 다 pandas DataFrame을 돌려주므로 합치는 비용이 거의 없습니다.
4. KIS REST API — 실거래와 같은 값을 쓸 때
무료 라이브러리로 만든 전략을 실제 봇에 올릴 때가 되면, 백테스트에 쓴 가격과 봇이 보는 가격이 같은 출처인가가 문제가 됩니다. 한국투자증권 KIS API에는 국내주식 기간별 시세를 주는 엔드포인트가 있어서, 이때 기준값을 맞추는 데 씁니다.
import requests
BASE = "https://openapi.koreainvestment.com:9443"
PATH = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice"
res = requests.get(BASE + PATH,
headers={
"authorization": f"Bearer {access_token}",
"appkey": APP_KEY,
"appsecret": APP_SECRET,
"tr_id": "FHKST03010100", # 국내주식기간별시세(일/주/월/년)
},
params={
"FID_COND_MRKT_DIV_CODE": "J", # J = 주식
"FID_INPUT_ISCD": "005930",
"FID_INPUT_DATE_1": "20250101",
"FID_INPUT_DATE_2": "20251231",
"FID_PERIOD_DIV_CODE": "D", # D=일 W=주 M=월 Y=년
"FID_ORG_ADJ_PRC": "0", # 수정주가/원주가 구분
})
print(res.json()["output2"][0])
응답은 다음과 같은 형태로, output2에 일자별 봉이 배열로 들어옵니다.
{
"stck_bsop_date": "20251229", // 영업일자
"stck_clpr": "72300", // 종가
"stck_oprc": "72800", // 시가
"stck_hgpr": "73100", // 고가
"stck_lwpr": "72200", // 저가
"acml_vol": "10553120", // 누적 거래량
"prdy_vrss": "-500" // 전일 대비
}
주의 — 대량 수집용 채널이 아니다. 이 엔드포인트는 한 번에 돌려주는 행 수에 제한이 있어 몇 년치를 받으려면 기간을 잘라 반복 호출해야 합니다. 그리고 KIS REST API에는 초당 호출 상한이 있어서, 루프를 그냥 돌리면 EGW00201(초당 거래건수 초과)이 떨어집니다. 자세한 대응은 API 호출 제한 설계를 보세요. appkey 발급 자체가 처음이라면 KIS API 발급 30분 완성부터입니다.
참고로 실시간 시세는 이 REST 엔드포인트가 아니라 WebSocket으로 받습니다. 과거 데이터(REST)와 실시간 틱(WebSocket)은 채널 자체가 다르다는 점을 처음부터 구분해 두세요 → KIS API 웹소켓 실시간 시세.
5. 데이터가 조용히 틀어지는 함정 4가지
여기부터가 이 글의 핵심입니다. 아래 넷은 에러를 내지 않습니다. 코드는 잘 돌고, 그래프도 그려지고, 성과지표도 나옵니다. 그런데 값이 틀립니다.
함정 ① 수정주가 vs 원주가
액면분할이나 무상증자가 있으면 주가는 하루아침에 몇 분의 1이 됩니다. 원주가는 그날 실제 거래된 가격 그대로라서 그 단절이 남아 있고, 수정주가는 과거를 소급 조정해 연속성을 맞춘 값입니다.
원주가로 백테스트를 돌리면 분할 당일에 -50%짜리 폭락 신호가 없던 데서 생깁니다. 추세추종 전략이라면 그 자리에서 전량 손절이 걸리고, 평균회귀 전략이라면 반대로 대량 매수가 걸립니다. 둘 다 현실에 없던 거래입니다.
라이브러리마다 기본값이 다르고, 같은 라이브러리도 내부 소스에 따라 다릅니다. pykrx는 adjusted 인자로 제어하고, FinanceDataReader는 어떤 소스를 타느냐에 따라 갈리며, KIS API는 FID_ORG_ADJ_PRC 값으로 구분합니다. "내가 지금 어느 쪽을 받고 있는지" 문서로 확인하지 않고 넘어가지 마세요. 값의 의미와 기본값은 버전에 따라 바뀔 수 있으므로 각 공식 문서 기준으로 확인하시기 바랍니다.
가장 빠른 자가진단: 과거에 액면분할이 있었던 종목을 하나 골라 그 전후 종가를 눈으로 보는 것입니다. 절벽처럼 떨어지는 구간이 있으면 원주가입니다.
함정 ② 생존편향 (상장폐지 종목이 없다)
fdr.StockListing("KRX")가 주는 것은 오늘 상장돼 있는 종목입니다. 이 리스트로 10년 백테스트를 돌리면, 그 10년 동안 망해서 사라진 회사들이 처음부터 없었던 것처럼 계산됩니다.
결과는 예측 가능합니다. 어떤 전략을 넣어도 성과가 실제보다 좋게 나옵니다. "저PBR 종목을 사서 버틴다" 같은 전략일수록 왜곡이 큽니다. 정말 망한 저PBR 종목들이 표본에서 빠져 있으니까요.
개인이 이걸 완벽히 해결하기는 어렵습니다. 현실적인 타협은 ① 테스트 기간을 짧게 잡고(최근 3~5년), ② 대형주 위주 유니버스로 제한하고, ③ 결과에 낙관 편향이 섞여 있다고 전제하는 것입니다. 백테스트 숫자를 얼마나 깎아서 봐야 하는지는 백테스트 +50%인데 실거래 -10%에서 다룹니다.
함정 ③ 티커 변경·합병·재상장
종목코드는 영구불변이 아닙니다. 회사가 합병하거나 인적분할하거나 지주사로 전환하면 코드가 바뀌거나 새로 생깁니다. 과거 데이터를 코드 기준으로만 이어붙이면 어느 날부터 데이터가 뚝 끊기거나, 전혀 다른 회사의 주가가 이어집니다.
증상은 대개 NaN이 뭉텅이로 생기거나, 수익률 계산에서 말도 안 되는 값이 튀는 형태로 나타납니다. 수집 직후 결측·이상치 점검을 자동화해 두는 게 최선입니다.
import numpy as np
def sanity_check(df, name=""):
issues = []
if df.isna().sum().sum() > 0:
issues.append(f"결측 {int(df.isna().sum().sum())}개")
ret = df["close"].pct_change()
spike = ret[ret.abs() > 0.35] # 일간 ±35% 초과
if len(spike):
issues.append(f"급변 {len(spike)}일: {list(spike.index[:3])}")
gap = df.index.to_series().diff().dt.days
if (gap > 10).any():
issues.append("10일 이상 데이터 공백 존재")
print(f"[{name}] " + (" / ".join(issues) if issues else "이상 없음"))
sanity_check(kr.rename(columns=str.lower), "FDR-005930")
함정 ④ 호출 제한과 차단
전 종목 몇 년치를 for문으로 긁으면 어딘가에서 막힙니다. 무료 라이브러리는 내부적으로 공개 페이지를 조회하는 구조라 과한 요청은 일시 차단으로 이어질 수 있고, 증권사 API는 초당 상한에 걸립니다.
실무적으로는 ①요청 사이에 간격을 두고 ②받은 데이터는 로컬(parquet·SQLite)에 캐시하고 ③다음 실행에서는 없는 구간만 채우는 증분 수집 구조를 씁니다. 이걸 안 만들어 두면 백테스트를 한 번 고칠 때마다 몇 시간씩 다시 긁게 됩니다.
import time, os
import pandas as pd
def fetch_cached(code, start, end, cache="data"):
os.makedirs(cache, exist_ok=True)
path = f"{cache}/{code}.parquet"
if os.path.exists(path):
return pd.read_parquet(path)
df = fdr.DataReader(code, start, end)
df.to_parquet(path)
time.sleep(0.3) # 연속 호출 간격
return df
수집·캐시·검증까지는 전략과 무관한 배관 작업입니다. 알고랩은 데이터 수집부터 백테스트, 실거래 봇까지 같은 로직으로 이어 붙여 제작합니다.
이 부분만 대신 만들어 드립니다 — 무료 상담 →6. 한눈 비교표와 실전 조합
pykrx | FinanceDataReader | KIS API | |
|---|---|---|---|
| 비용 | 무료 | 무료 | 무료(계좌 필요) |
| 국내주식 OHLCV | O | O | O |
| 해외주식·환율 | X | O | 별도 엔드포인트 |
| 종목 리스트 일괄 | O | O (StockListing) | 제한적 |
| 시가총액·수급 | O (강점) | 제한적 | 일부 |
| 실거래와 동일 출처 | X | X | O |
| 대량 수집 | 느림 | 보통 | 제한 큼 |
실전 조합 — ① 탐색·백테스트: FinanceDataReader로 가격, pykrx로 시가총액·수급 → pandas에서 결합. ② 검증: 두 소스 종가 대조 + 위 sanity_check. ③ 실거래 직전: KIS API로 최근 구간만 받아 백테스트 값과 일치하는지 최종 확인. ④ 운영: 실시간은 WebSocket, 과거는 캐시된 parquet.
7. 두 소스 교차검증 코드
데이터를 믿을 수 있는지 판단하는 가장 값싼 방법은 서로 다른 두 곳에서 같은 것을 받아 비교하는 것입니다. 종가가 어긋나는 날짜가 나오면 그 날짜를 직접 확인하면 됩니다.
from pykrx import stock
import FinanceDataReader as fdr
import pandas as pd
code, s, e = "005930", "20250101", "20251231"
a = stock.get_market_ohlcv(s, e, code)["종가"].rename("pykrx")
b = fdr.DataReader(code, "2025-01-01", "2025-12-31")["Close"].rename("fdr")
cmp = pd.concat([a, b], axis=1).dropna()
cmp["diff_pct"] = (cmp["pykrx"] - cmp["fdr"]).abs() / cmp["fdr"] * 100
bad = cmp[cmp["diff_pct"] > 0.5] # 0.5% 넘게 벌어진 날
print(f"비교 {len(cmp)}일 중 불일치 {len(bad)}일")
print(bad.head())
불일치가 특정 날짜에 몰려 있으면 수정주가 처리 차이, 전 구간에 고르게 퍼져 있으면 소스 자체가 다른 것입니다. 전자는 옵션을 맞추면 해결되고, 후자는 어느 쪽을 기준으로 삼을지 결정해야 합니다.
8. 데이터가 좋아도 남는 문제
데이터를 깨끗하게 만들었다고 해서 백테스트 결과가 믿을 만해지는 것은 아닙니다. 같은 데이터로 수백 번 조합을 돌려 제일 좋은 것만 고르면, 그건 데이터 품질과 무관하게 착시입니다(→ 백테스트가 화려할수록 왜 의심해야 할까).
그래서 데이터 정리 다음 순서는 워크포워드 검증과 성과지표를 제대로 읽는 것입니다. CAGR 하나만 보지 말고 MDD·샤프지수·거래횟수를 같이 봐야 한다는 이야기는 백테스트 리포트 읽는 법에 정리돼 있습니다. 파이썬 환경 구성부터 처음이라면 파이썬 자동매매 입문 가이드가 출발점입니다.
본문의 라이브러리·API 관련 내용은 정량적 데이터 처리 방법을 설명하는 것이며 특정 종목이나 투자 판단을 권유하지 않습니다. 백테스트는 과거 데이터에 대한 시뮬레이션일 뿐 미래 성과를 보장하지 않습니다. 각 라이브러리·API의 스펙과 기본값은 버전·정책에 따라 변경되므로 공식 문서에서 확인하시기 바랍니다.
자주 묻는 질문
Q. pykrx와 FinanceDataReader 중 뭘 쓰나요?
국내 전용 + 시가총액·수급이 필요하면 pykrx, 국내·해외를 함께 다루고 종목 리스트가 필요하면 FinanceDataReader. 둘 다 pandas를 돌려주니 병행이 정답입니다.
Q. 수정주가와 원주가, 뭘 써야 하나요?
백테스트는 수정주가가 기본입니다. 원주가를 쓰면 액면분할 자리에 없던 폭락 신호가 생깁니다.
Q. 생존편향은 어떻게 줄이나요?
완전히 없애긴 어렵습니다. 기간을 짧게, 유니버스를 대형주로 제한하고, 결과에 낙관 편향이 있다고 전제하세요.
Q. 증권사 API로 과거 데이터를 대량 수집해도 되나요?
행 수·초당 호출 제한이 있어 대량 수집에는 부적합합니다. 과거는 무료 라이브러리로, 증권사 API는 기준값 대조와 실거래에 쓰는 편이 낫습니다.
마무리
자동매매에서 데이터는 재료가 아니라 절반입니다. 전략 코드는 몇 줄이면 바꾸지만, 잘못된 데이터로 나온 결론은 무엇을 바꿔도 계속 틀립니다. pykrx든 FinanceDataReader든 KIS API든, "내가 지금 수정주가를 보고 있는가"와 "이 종목 리스트에 사라진 회사가 들어 있는가" 두 질문에 답할 수 있으면 절반은 넘어간 것입니다.
수집·캐시·검증 배관을 직접 만들 시간에 전략에 집중하고 싶다면, 알고랩이 데이터 파이프라인부터 백테스트·실거래 봇까지 하나의 로직으로 이어 제작해 드립니다.
데이터부터 실거래까지, 한 줄로 이어 드립니다
수집·검증·백테스트·주문 연동을 따로 만들면 값이 어긋납니다. 알고랩은 같은 로직으로 통합 제작합니다.
24시간 빠른 답변 가능합니다.