4 minute read

지난 글에서 “지금의 지수 구성종목으로 과거를 백테스트하면 안 된다”고 했습니다. 그 문제의 뿌리가 오늘 다룰 생존 편향(survivorship bias)입니다. 백테스트를 망가뜨리는 편향 중 가장 조용하고, 무료 데이터로 작업하는 개인 투자자가 가장 피하기 어려운 것이기도 합니다.

돌아온 폭격기의 총알 자국

생존 편향을 설명할 때 빠지지 않는 이야기가 있습니다. 2차 세계대전 때 미군은 귀환한 폭격기의 총알 자국을 조사해 장갑을 보강할 위치를 정하려 했습니다. 날개와 동체에 자국이 집중되어 있었으니 거기를 보강하자는 것이 자연스러운 결론이었죠. 통계학자 에이브러햄 왈드는 반대로 답했습니다. 자국이 없는 곳, 즉 엔진과 조종석을 보강해야 한다고요. 그곳에 맞은 비행기는 돌아오지 못했기 때문에 데이터에 없었던 겁니다.

백테스트에도 똑같은 일이 벌어집니다. 지금 데이터에 남아 있는 종목은 돌아온 폭격기입니다. 상장폐지되어 사라진 종목은 데이터에 없고, 그래서 우리는 엔진에 맞은 비행기가 얼마나 많았는지 모릅니다.

백테스트에서 생기는 방식

가장 흔한 경로는 이렇습니다. “지금 상장된 전 종목 목록”을 받아서, 그 종목들의 10년 치 가격을 내려받고, 그 위에서 전략을 돌립니다. 지난 글에서 다룬 FinanceDataReader의 StockListing('KRX')가 정확히 이런 목록을 줍니다. 편리하지만, 이 목록에는 지난 10년 사이 상장폐지된 종목이 한 개도 없습니다.

그 결과 백테스트의 유니버스는 “10년을 살아남은 종목들”로만 구성됩니다. 부도난 회사, 감사의견 거절로 퇴출된 회사, 다른 회사에 흡수된 회사는 모두 빠져 있습니다. 10년 전 시점에서 보면, 앞으로 망하지 않을 종목만 골라서 투자한 셈입니다. 성과가 좋을 수밖에 없습니다.

특히 심하게 왜곡되는 전략이 있습니다.

  • 밸류 전략 — PBR이 낮은 종목은 “싸서 좋은 회사”와 “망해가는 회사”가 섞여 있습니다. 후자가 데이터에서 사라지면 밸류 전략은 실제보다 훨씬 좋아 보입니다.
  • 소형주 전략 — 상장폐지는 소형주에서 압도적으로 많이 일어납니다. 소형주 유니버스일수록 생존자 편향이 큽니다.
  • 역발상·평균회귀 전략 — “많이 떨어진 종목을 산다”는 규칙은 상장폐지 직전 종목을 정확히 골라냅니다. 그 종목들이 데이터에 없으면 폭락 후 반등한 종목만 남습니다.

반대로 대형주·ETF·지수를 다루는 전략은 상대적으로 안전합니다. 지금까지 이 시리즈가 코스피 지수 하나로 백테스트를 해온 이유 중 하나이기도 합니다.

왜곡의 크기

정확한 수치는 시장과 기간, 전략마다 다릅니다. 다만 학계 연구에서 생존 편향을 보정하기 전후의 성과 차이가 연 1%p를 넘는 경우가 흔히 보고되고, 소형주나 밸류 전략에서는 그보다 훨씬 큰 차이가 나기도 합니다. 연 1%p는 작아 보이지만, 20년 복리로는 최종 자산의 20% 이상 차이입니다. “이 전략이 시장을 연 1.5%p 이겼다”는 백테스트 결과가 생존 편향 하나로 통째로 사라질 수 있다는 뜻입니다.

무료 데이터로 할 수 있는 것

완벽한 해결책은 각 시점의 상장 종목 목록과, 상장폐지된 종목의 가격 데이터를 모두 갖추는 것입니다. 이런 데이터는 유료이거나 구축에 손이 많이 갑니다. 무료 데이터로 작업할 때 현실적인 대응은 세 단계입니다.

1단계: 시점별 상장 목록 만들기

거래소 정보 시스템에서는 상장폐지 종목 목록과 상장일·폐지일을 조회할 수 있습니다. 이걸로 “그 시점에 상장되어 있던 종목”을 재구성합니다.

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(폐지 시 손실률)는 가정치입니다. 거래소 통계로 실제 폐지율을 확인해서 넣으면 더 현실적인 추정이 됩니다. 이 방법은 정확한 답을 주지는 못하지만, “이 전략은 생존 편향에 얼마나 민감한가”라는 질문에는 답을 줍니다. 두 곡선의 차이가 크면 그 전략은 실전에서 크게 실망시킬 가능성이 높습니다.

3단계: 편향이 작은 유니버스를 쓰기

가장 실용적인 방법입니다. 시가총액 상위 종목, 지수, ETF처럼 상장폐지가 드문 대상으로 전략을 설계하면 생존 편향의 영향이 구조적으로 작아집니다. 소형주 전략을 무료 데이터로 검증하려는 시도는, 정직하게 말하면 신뢰할 만한 결과를 얻기 어렵습니다.

다른 얼굴들

생존 편향은 종목에만 있는 것이 아닙니다.

  • 펀드 성과 — “국내 주식형 펀드의 10년 평균 수익률”에는 중간에 청산된 펀드가 빠져 있습니다. 성과가 나쁜 펀드가 주로 청산되므로 평균이 부풀려집니다.
  • 전략 자체 — 블로그나 책에 소개되는 전략은 살아남은 전략입니다. 실패한 전략은 아무도 글로 쓰지 않습니다. 이 블로그의 “삽질 기록” 카테고리를 만든 이유가 이것입니다.
  • 투자 대가의 조언 — 성공한 투자자의 습관을 따라 하는 것도 같은 문제를 안고 있습니다. 같은 습관으로 실패한 사람들은 인터뷰 대상이 되지 않습니다.

정리

  • 생존 편향은 데이터에서 사라진 종목을 무시해서 성과가 부풀려지는 현상이며, 지금 상장 목록으로 과거를 백테스트하면 반드시 발생한다.
  • 밸류·소형주·역발상 전략에서 특히 심하고, 대형주·지수·ETF 전략에서는 상대적으로 작다.
  • 무료 데이터로는 시점별 상장 목록 재구성, 상장폐지 스트레스 테스트, 편향이 작은 유니버스 선택으로 대응한다.
  • 종목뿐 아니라 펀드 성과, 소개되는 전략, 성공담에도 같은 편향이 있다.

이것으로 백테스트의 3대 함정 — 미래 참조, 과최적화, 생존 편향 — 을 모두 다뤘습니다. 다음 글부터는 방향을 바꿔서 자산 배분으로 넘어갑니다. 주식과 채권을 섞으면 왜 수익률 대비 위험이 줄어드는지, 그 원리를 데이터로 확인하는 것부터 시작하겠습니다.


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

Updated: