3 분 분량

이 블로그의 세 글이 같은 질문에 서로 다른 답을 적었습니다. “1995년 5월부터 코스피를 사서 들고 있었다면 연복리는 얼마였나”라는 질문입니다.

글 데이터 마지막 날 그날 코스피 보유 연복리
실전 ⑧ 추세추종과 손절 2026-09-17 6,715.4 6.60%
최고의 날 10일을 놓치면 2026-10-14 6,008.8 6.20%
변동성 타기팅 2026-10-19 5,768.0 6.06%

코드와 데이터 출처는 같고, 다른 것은 데이터가 끝나는 날짜뿐입니다. 9월 17일부터 10월 19일까지 지수가 14.1% 내렸고, 그 한 달이 31년짜리 연복리를 0.5%p 넘게 끌어내렸습니다. 31년 중 마지막 한 달이 이만큼 영향을 준다는 점은 예상하지 못했습니다. 세 글 모두 본문에 데이터 기간을 적어 두기는 했지만, 글을 건너 읽는 사람에게는 같은 질문에 답이 셋인 것처럼 보였을 겁니다.

날짜가 같아도 숫자가 달랐다

진짜 문제는 그다음이었습니다. 실전 ⑧의 스크립트를 2026년 9월 17일까지로 잘라 다시 돌렸더니 거래일 수(7,907일), 변동성(26.31%), 최대 낙폭(-72.46%)은 모두 그대로였습니다. 그런데 연복리만 6.60%가 아니라 6.59%로 나왔습니다.

차이는 0.01%p라 결론에는 영향이 없습니다. 문제는 왜 달라졌는지 지금은 확인할 방법이 없다는 점이었습니다. 그때 받은 데이터를 저장해 두지 않았으니 어느 날의 값이 바뀌었는지 비교할 수 없습니다. 장중에 받으면 마지막 날의 “종가”가 아직 확정되지 않은 값일 수 있는데, 이번이 그런 경우였는지도 알 수 없습니다. 데이터를 받아서 바로 계산만 하고 버리는 방식으로는 이런 차이를 설명할 수 없다는 것을 이때 알았습니다. 실전 ⑧은 그 뒤 코드에 데이터 종료일을 넣고, 다시 돌린 값(6.59%)으로 표를 고쳤습니다.

끝나지 않은 달

비슷한 일을 하나 더 겪었습니다. 일시 투자와 분할 투자 글을 쓰면서 일별 종가를 월말 종가로 바꿨는데, 그때 데이터는 9월 17일에서 끝나 있었습니다.

import FinanceDataReader as fdr

close = fdr.DataReader('KS11', '1995-01-01')['Close']
print(close.loc[:'2026-09-17'].resample('ME').last().tail(2))
Date
2026-08-31    6820.02
2026-09-30    6715.41
Freq: ME, Name: Close, dtype: float64

9월 17일 종가가 “9월 30일” 이름표를 달고 나옵니다. pandas는 그 달의 마지막 값을 월말로 묶을 뿐, 그 달이 끝났는지는 확인하지 않습니다. 이대로 두면 9월은 반달짜리 수익률이 한 달 수익률인 척 계산에 들어갑니다. 발행 전에 발견해서 그 글은 2026년 8월까지만 썼습니다.

조용히 멈춘 사본

밸류 팩터 글과 올웨더 글에서 쓴 파마-프렌치 팩터와 실러 데이터는 원본이 아니라 공개 저장소에 올라와 있는 사본입니다. 원본 자료에는 그 뒤의 기간도 있지만, 사본은 각각 2020년 7월과 2023년 6월에서 멈춰 있습니다. 오류는 나지 않습니다. 코드는 멀쩡히 돌고, 결과에 그 뒤의 기간이 빠져 있을 뿐입니다. 그래서 이 블로그는 이 데이터를 쓴 글마다 첫 데이터 문단에 끝나는 달을 적고 있습니다.

무료 데이터가 바뀌는 방식은 이 밖에도 여럿입니다. 응답 형식이 바뀌어 코드가 멈추기도 하고, 로그인이 필요해지거나 서비스가 끝나기도 합니다. 개별 종목의 수정주가는 배당이나 액면분할이 생길 때마다 과거 값이 다시 계산됩니다. 미래 참조 편향 글에서 다룬 문제입니다. 받을 때마다 과거가 조금씩 다를 수 있다는 뜻입니다.

그래서 바꾼 것

데이터를 받는 부분을 작은 도구로 따로 뺐습니다. 전체 코드는 data_cache.py에 있고, 하는 일은 다섯 가지입니다.

  1. 받은 데이터를 저장합니다. 최신본 하나와 함께 마지막 날짜를 붙인 사본도 남깁니다. 다음에 숫자가 달라지면 무엇이 바뀌었는지 비교할 수 있습니다.
  2. 저장해 둔 사본과 비교합니다. 겹치는 날짜의 값이 바뀌었거나 예전에 있던 날이 빠졌으면 알립니다.
  3. 이상한 곳을 점검합니다. 빈 데이터, 날짜 중복, 열흘 넘는 공백, 하루 ±30%를 넘는 변동을 찾습니다.
  4. 받기에 실패하면 저장해 둔 사본으로 계속합니다. 대신 사본을 쓰고 있다고 알립니다.
  5. 결과 옆에 적을 정보를 출력합니다. 기간, 행 수, 그리고 데이터 내용으로 만든 짧은 해시(“지문”)입니다. 지문이 같으면 같은 데이터로 계산한 숫자입니다.

핵심은 마지막 줄입니다.

import hashlib
import FinanceDataReader as fdr

close = fdr.DataReader('KS11', '1995-01-01')['Close']
fp = hashlib.sha1(close.round(6).to_csv().encode()).hexdigest()[:10]
print(f'{close.index[0].date()} ~ {close.index[-1].date()}, {len(close)}행, 지문 {fp}')

월말 종가를 만드는 함수는 데이터의 마지막 날 다음 영업일이 같은 달이면 그 달을 뺍니다. 한국 휴장일은 모르기 때문에, 데이터가 연말 마지막 거래일에 끝나면 12월 31일 휴장일을 영업일로 착각해 다 끝난 12월을 뺄 수 있습니다. 그래도 끝나지 않은 달을 넣는 것보다는 끝난 달을 빼는 쪽이 덜 위험하다고 보고 그대로 뒀습니다.

코스피로 돌려 본 결과입니다(2026년 10월 21일까지).

점검: 2017-10-10 앞에 11일 공백
KS11: 1995-05-02 ~ 2026-10-21, 7927행, 지문 2d1a1300fe
월말 종가 377개월, 마지막 2026-09-30

점검에 하나가 걸렸습니다. 확인해 보니 2017년 추석 연휴입니다. 9월 29일 다음 거래일이 10월 10일이었습니다. 데이터 오류가 아니라 실제 휴장이었습니다. 점검은 이상해 보이는 곳을 알려 줄 뿐이고, 그것이 정말 문제인지는 사람이 확인해야 합니다.

앞으로 이 블로그의 결과표에는 데이터 마지막 날을 계속 적겠습니다. 숫자가 글마다 조금씩 다르다면 대개 기간이 달라서입니다. 그래도 설명이 안 되는 차이가 보이면 문의 페이지로 알려 주시면 확인하겠습니다.


이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 과거 데이터로 계산한 결과는 미래 성과를 보장하지 않습니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.

최종 수정: