퀀트 투자 입문 ⑩ — 생존 편향, 사라진 종목들이 만드는 착시
지난 글에서 “지금의 지수 구성종목으로 과거를 백테스트하면 안 된다”고 했습니다. 그 문제의 뿌리가 오늘 다룰 생존 편향(survivorship bias)입니다. 백테스트를 망가뜨리는 편향 중 가장 조용하고, 무료 데이터로 작업하는 개인 투자자가 가장 피하기 어려운 것이기도 합니다. 퀀트 투자란 무엇인가에서 “의심하는 습관”이라고 쓴 것의 구체적인 사례입니다.
돌아온 폭격기의 총알 자국
생존 편향을 설명할 때 빠지지 않는 이야기가 있습니다. 2차 세계대전 때 미군은 귀환한 폭격기의 총알 자국을 조사해 장갑을 보강할 위치를 정하려 했습니다. 날개와 동체에 자국이 집중되어 있었으니 거기를 보강하자는 것이 자연스러운 결론이었죠. 통계학자 에이브러햄 왈드는 반대로 답했습니다. 자국이 없는 곳, 즉 엔진과 조종석을 보강해야 한다고요. 그곳에 맞은 비행기는 돌아오지 못했기 때문에 데이터에 없었던 겁니다.
백테스트에도 똑같은 일이 벌어집니다. 지금 데이터에 남아 있는 종목은 돌아온 폭격기입니다. 상장폐지되어 사라진 종목은 데이터에 없고, 그래서 우리는 엔진에 맞은 비행기가 얼마나 많았는지 모릅니다.
백테스트에서 생기는 방식
가장 흔한 경로는 이렇습니다. “지금 상장된 전 종목 목록”을 받아서, 그 종목들의 10년 치 가격을 내려받고, 그 위에서 전략을 돌립니다. FinanceDataReader 글에서 다룬 StockListing('KRX')가 정확히 이런 목록을 줍니다. 편리하지만, 이 목록에는 지난 10년 사이 상장폐지된 종목이 한 개도 없습니다.
그 결과 백테스트의 유니버스는 “10년을 살아남은 종목들”로만 구성됩니다. 부도난 회사, 감사의견 거절로 퇴출된 회사, 다른 회사에 흡수된 회사는 모두 빠져 있습니다. 10년 전 시점에서 보면, 앞으로 망하지 않을 종목만 골라서 투자한 셈입니다. 성과가 좋을 수밖에 없습니다.
특히 심하게 왜곡되는 전략이 있습니다.
- 밸류 전략 — PBR이 낮은 종목은 “싸서 좋은 회사”와 “망해가는 회사”가 섞여 있습니다. 후자가 데이터에서 사라지면 밸류 전략은 실제보다 훨씬 좋아 보입니다.
- 소형주 전략 — 상장폐지는 소형주에서 압도적으로 많이 일어납니다. 소형주 유니버스일수록 생존 편향이 큽니다.
- 역발상·평균회귀 전략 — “많이 떨어진 종목을 산다”는 규칙은 상장폐지 직전 종목을 정확히 골라냅니다. 그 종목들이 데이터에 없으면 폭락 후 반등한 종목만 남습니다.
반대로 대형주·ETF·지수를 다루는 전략은 상대적으로 안전합니다. 지금까지 이 시리즈가 코스피 지수 하나로 백테스트를 해온 이유 중 하나이기도 합니다.
왜곡의 크기
정확한 크기는 시장과 기간, 전략, 그리고 폐지 종목을 어떻게 처리하느냐에 따라 달라서 숫자 하나로 말하기 어렵습니다. 감을 잡기 위해 생존 편향이 성과를 연 1%p 부풀린다고 해 보겠습니다. 연 1%p는 작아 보이지만, 20년 복리로는 최종 자산이 약 20% 차이 납니다. “이 전략이 시장을 연 1.5%p 이겼다”는 백테스트 결과도 편향이 조금만 더 크면 통째로 사라질 수 있다는 뜻입니다. 폐지가 잦은 소형주나 저PBR 종목을 많이 담는 전략일수록 이 차이는 더 커질 수 있습니다. 한국 데이터로 직접 재 본 결과는 아래에 따로 정리했습니다.
무료 데이터로 할 수 있는 것
완벽한 해결책은 각 시점의 상장 종목 목록과, 상장폐지된 종목의 가격 데이터를 모두 갖추는 것입니다. 이런 데이터는 유료이거나 구축에 손이 많이 갑니다. 무료 데이터로 작업할 때 현실적인 대응은 세 단계입니다.
1단계: 시점별 상장 목록 만들기
상장폐지 종목 목록과 상장일·폐지일은 한국거래소 정보데이터시스템에서 조회할 수 있고, 파이썬에서는 FinanceDataReader의 fdr.StockListing('KRX-DELISTING')으로 받을 수 있습니다. 다만 이 목록에는 폐지된 종목만 있으므로, 현재 상장 종목의 상장일(fdr.StockListing('KRX-DESC')의 ListingDate)과 합치고 열 이름(Symbol, ListingDate, DelistingDate)을 아래 코드에 맞게 바꿔야 listing 표가 됩니다. 이 표로 “그 시점에 상장되어 있던 종목”을 재구성합니다.
import pandas as pd
# listing: 종목별 상장일·상장폐지일 (폐지되지 않았으면 NaT)
# columns: code, listed_date, delisted_date
def universe_at(listing, date):
"""date 시점에 상장되어 있던 종목 코드 목록"""
date = pd.Timestamp(date)
alive = (listing['listed_date'] <= date) & (
listing['delisted_date'].isna() | (listing['delisted_date'] > date)
)
return listing.loc[alive, 'code'].tolist()
이 함수를 리밸런싱 시점마다 호출해서 유니버스를 정하면, 적어도 “그때 존재하지 않았던 종목”을 사는 실수는 막을 수 있습니다. 문제는 폐지된 종목의 가격 데이터가 여전히 없다는 것인데, 그건 다음 단계에서 다룹니다.
2단계: 스트레스 테스트로 영향 가늠하기
폐지 종목 가격을 구할 수 없다면, “만약 있었다면”을 가정해서 성과가 얼마나 나빠지는지 확인합니다. 매년 일정 비율의 종목이 큰 손실과 함께 사라진다고 가정하고 백테스트를 다시 돌리는 방식입니다.
import numpy as np
import pandas as pd
def survivorship_stress(returns_df, delist_rate_annual=0.03,
delist_loss=-0.6, seed=0):
"""
returns_df: 종목별 월간 수익률 (columns = 종목)
매월 일부 종목을 무작위로 '상장폐지'시켜 그 달에 delist_loss를 부여하고
이후 데이터는 제거한다. 생존 편향이 있었다면 성과가 얼마나 부풀려졌는지 가늠하는 용도.
"""
rng = np.random.default_rng(seed)
r = returns_df.copy()
monthly_rate = delist_rate_annual / 12
alive = pd.Series(True, index=r.columns)
for i, dt in enumerate(r.index):
candidates = alive[alive].index
n_delist = rng.binomial(len(candidates), monthly_rate)
if n_delist == 0:
continue
victims = rng.choice(candidates, size=n_delist, replace=False)
r.loc[dt, victims] = delist_loss
r.loc[r.index[i + 1:], victims] = np.nan
alive[victims] = False
return r
# 동일가중 포트폴리오로 비교 (NaN은 자동 제외 = 생존 종목에 재투자)
eq_original = returns_df.mean(axis=1)
eq_stressed = survivorship_stress(returns_df).mean(axis=1)
cum = pd.DataFrame({
'Survivors only': (1 + eq_original).cumprod(),
'With delistings': (1 + eq_stressed).cumprod(),
})
print(cum.iloc[-1].round(3))
delist_rate_annual(연간 상장폐지율)과 delist_loss(폐지 시 손실률)는 가정치입니다. 거래소 통계로 실제 폐지율을 확인해서 넣으면 더 현실적인 추정이 됩니다. 이 방법은 정확한 답을 주지는 못하지만, “생존 편향이 성과를 얼마나 부풀렸을까”를 가늠해 볼 수는 있습니다. 다만 위 코드는 모든 종목이 같은 확률로 폐지된다고 가정하므로 어떤 종목을 골라도 차이가 비슷하게 나옵니다. 전략별 민감도를 보려면 소형주·저PBR·급락 종목처럼 폐지가 잦은 종목에 더 높은 폐지율을 주어야 합니다. 폐지율을 종목 특성에 맞게 달리 준 뒤에도 두 곡선의 차이가 크다면, 그 전략은 실전에서 크게 실망시킬 가능성이 높습니다.
3단계: 편향이 작은 유니버스를 쓰기
가장 실용적인 방법입니다. 시가총액 상위 종목, 지수, ETF처럼 상장폐지가 드문 대상으로 전략을 설계하면 생존 편향의 영향이 구조적으로 작아집니다. 소형주 전략을 무료 데이터로 검증해서는, 정직하게 말하면 신뢰할 만한 결과를 얻기 어렵습니다.
한국 데이터로 재어 본 결과
실전 시리즈의 워크포워드 검증 글에서 한국 주식으로 이 편향을 직접 재 봤습니다. pykrx의 기간 등락률 함수(get_market_price_change_by_ticker)는 기본값(delist=False)으로 두면 상장폐지 종목을 등락률 −100으로 전 종목 표에 합쳐 줍니다. 1단계에서 말한 “폐지된 종목의 가격 데이터”의 빈자리를, 적어도 폐지 시점의 손실만큼은 무료 데이터로 메울 수 있는 셈입니다. 다만 −100은 가치가 0이 됐다는 가정이라 인수합병이나 재상장인 경우에는 손실을 과하게 잡습니다. 또 이름과 달리 delist=True는 상장폐지 종목만 돌려주니 주의해야 합니다.
2024년 12월 말부터 2026년 9월까지 21개 구간에서 상장폐지가 162건 잡혔고, 그중 시가총액 1,000억 원 이상 유니버스 안에 든 것은 31건이었습니다. 그런데 밸류 점수(이익수익률·저PBR·배당)로 매달 뽑은 20종목 안에는 한 건도 없어서, 폐지 종목을 −100%로 넣은 경우와 아예 뺀 경우의 차이는 0.0%p였습니다. 시가총액 하한이 3단계의 “편향이 작은 유니버스” 역할을 한 셈입니다.
그렇다고 생존 편향이 걱정할 일이 아니라는 뜻은 아닙니다. 21개월은 짧은 표본이고, 시가총액 하한을 낮추거나 기간을 늘리면 결과가 달라질 수 있습니다. 편향이 있는지는 짐작하지 말고 그때그때 재어 보는 것이 맞습니다.
다른 얼굴들
생존 편향은 종목에만 있는 것이 아닙니다.
- 펀드 성과 — “국내 주식형 펀드의 10년 평균 수익률”에는 중간에 청산된 펀드가 빠져 있습니다. 성과가 나쁜 펀드가 주로 청산되므로 평균이 부풀려집니다.
- 전략 자체 — 블로그나 책에 소개되는 전략은 살아남은 전략입니다. 실패한 전략은 아무도 글로 쓰지 않습니다. 이 블로그가 리스크 패리티 글의 “삽질 기록”이나 워크포워드 검증 글의
delist옵션 실수처럼 실패한 시도까지 남기려는 이유가 이것입니다. - 투자 대가의 조언 — 성공한 투자자의 습관을 따라 하는 것도 같은 문제를 안고 있습니다. 같은 습관으로 실패한 사람들은 인터뷰 대상이 되지 않습니다.
정리
- 생존 편향은 데이터에서 사라진 종목을 무시해서 성과가 부풀려지는 현상이며, 지금 상장 목록으로 과거를 백테스트하면 반드시 발생한다.
- 밸류·소형주·역발상 전략에서 특히 심하고, 대형주·지수·ETF 전략에서는 상대적으로 작다.
- 무료 데이터로는 시점별 상장 목록 재구성, 상장폐지 스트레스 테스트, 편향이 작은 유니버스 선택으로 대응한다.
- 종목뿐 아니라 펀드 성과, 소개되는 전략, 성공담에도 같은 편향이 있다.
이것으로 백테스트의 3대 함정 — 미래 참조, 과최적화, 생존 편향 — 을 모두 다뤘습니다. 다음 글부터는 방향을 바꿔서 자산 배분으로 넘어갑니다. 주식과 채권을 섞으면 왜 수익률 대비 위험이 줄어드는지, 그 원리를 데이터로 확인하는 것부터 시작하겠습니다.
2026-09-28 수정: 무작위 상장폐지 스트레스 테스트가 전략별 민감도를 보여 준다고 쓴 설명을, 모든 종목에 같은 폐지율을 주므로 부풀림의 크기만 가늠할 수 있다는 내용으로 고치고, 실제로는 없던 “삽질 기록” 카테고리를 언급한 문장을 바로잡았습니다.
이 글은 학습과 정보 공유 목적으로 작성되었으며, 특정 종목이나 상품에 대한 투자 권유가 아닙니다. 투자의 판단과 책임은 투자자 본인에게 있습니다.