Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

주식 데이터의 생존자 편향이란 무엇인가

주식 데이터의 생존자 편향은 상장 폐지된 종목이 데이터셋에서 누락되어 발생하는 오류입니다. 백테스트 결과의 왜곡을 방지하기 위해 전체 유니버스를 측정하는 방법과 생존자 편향이 발생하는 원인을 상세히 설명합니다.

주식 데이터의 생존자 편향(survivorship bias)은 현재 상장된 종목만을 포함하는 파일에서 발생하는 격차를 의미합니다. 상장 폐지, 인수합병, 사명 변경 또는 청산된 모든 기업은 데이터에서 조용히 사라지며, 이러한 데이터셋을 기반으로 백테스트를 수행하면 시장의 실제 모습이 아닌 필터링된 과거의 표본만을 측정하게 됩니다. 파일 자체에는 오류가 없어 보입니다. 모든 행에는 실제 가격이 기록되어 있으나, 문제는 기록되지 않은 행들에 있습니다.

주식 데이터에서 생존자 편향이란 무엇인가?

"미국 전체 주식" 데이터셋이 어떻게 구성되는지 생각해 보십시오. 데이터 제공업체에 심볼 리스트를 요청하면, 해당 리스트에 있는 각 종목의 가격 이력을 가져옵니다. 이 리스트는 구조적으로 현재 시점을 기준으로 합니다. 이는 "현재 거래되는 종목은 무엇인가"라는 질문에 답하는 것이며, 요청 시점 이전에 거래가 중단된 모든 심볼과 그 전체 이력은 답변에서 누락됩니다.

이 용어는 1943년 에이브러햄 월드(Abraham Wald)가 귀환한 폭격기들을 연구하며 제시한 개념에서 유래했습니다. 살아 돌아온 항공기에는 총탄 자국이 없는 부위에 장갑을 보강해야 했습니다. 총탄을 맞은 부위의 항공기들은 돌아오지 못해 측정할 수 없었기 때문입니다. 오늘날 상장된 종목들로 구축된 주식 데이터베이스도 같은 형태를 띱니다. 우리가 볼 수 있는 표본은 살아남은 표본이며, 표본에 포함되는지 여부는 백테스트가 측정하려는 바로 그 결과에 의해 결정됩니다.

매년 얼마나 많은 티커가 사라지는가?

데이터의 누락 규모부터 살펴보겠습니다. 아래 패널은 일별 주식 거래 데이터의 모든 심볼을 최종 거래일의 연도별로 그룹화한 것이며, 2026년 1월 이전에 이미 거래가 중단된 심볼만을 포함합니다.

조회연도별 상장 폐지 종목 수
모든 수치 뒤에 숨겨진 정확한 SQL
SELECT
    toString(toYear(last_bar))  AS year,
    count()                     AS gone_dark_count,
    round(avg(bars_on_tape))    AS avg_bars_on_tape
FROM
(
    SELECT
        ticker,
        max(date) AS last_bar,
        count()   AS bars_on_tape
    FROM global_markets.stocks_daily_aggs
    WHERE date >= '2015-01-01'
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker
    HAVING max(date) >= toDate('2016-01-01')
       AND max(date) <  toDate('2026-01-01')
)
GROUP BY year
ORDER BY year
Run this yourself

2025년에는 1283개의 심볼이 마지막 일별 종가를 기록한 후 다시 나타나지 않았습니다. 이들은 일주일 된 껍데기 기업이 아닙니다. 해당 그룹은 2015년 이후 평균 999일의 일별 종가를 기록했습니다. 패널에 포함된 10년 동안 이 수치는 변동하며, 어느 해에도 0에 가깝지 않습니다. 심볼이 사라지는 이유는 인수합병 완료, 펀드 청산, 상장 폐지 기준 미달 등 일반적인 사유들입니다.

10줄짜리 스크립트로 편향 확인하기

산술적인 계산은 설명보다 확인이 쉽습니다. 아래 스크립트는 두 개의 작은 CSV 파일을 작성합니다. 하나는 생존 종목만 포함한 유니버스이고, 다른 하나는 거래가 중단된 종목까지 모두 포함한 유니버스입니다. 각 파일의 평균을 구하고 그 차이를 출력합니다. 파이썬 표준 라이브러리만 사용하므로 별도의 설치가 필요 없습니다.

python3 <<'PY'
import csv, statistics

survivors = [("AAA", 0.42), ("BBB", 0.18), ("CCC", 0.63), ("DDD", 0.07)]
delisted  = [("EEE", -1.00), ("FFF", -0.55), ("GGG", 0.31)]

def write_csv(path, rows):
    with open(path, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["ticker", "total_return"])
        w.writerows(rows)

def mean_return(path):
    with open(path, newline="") as f:
        return statistics.mean(float(r["total_return"]) for r in csv.DictReader(f))

write_csv("survivors_only.csv", survivors)
write_csv("full_universe.csv", survivors + delisted)

a = mean_return("survivors_only.csv")
b = mean_return("full_universe.csv")
print(f"survivors only: {a:+.1%} across {len(survivors)} names")
print(f"full universe: {b:+.1%} across {len(survivors) + len(delisted)} names")
print(f"survivorship bias: {a - b:+.1%}")
PY

스크립트의 7개 수익률 수치는 시장 데이터가 아닌 예시입니다. 메커니즘을 시각화하기 위해 작성되었습니다. 생존 종목만 평균을 내면 +32.5%이지만, 전체 유니버스는 +0.9%이며, 31.6%포인트의 차이가 바로 편향입니다. 생존 종목 파일의 데이터가 거짓인 것은 아닙니다. 4개 종목은 실제로 기록된 수익률을 냈습니다. 다만 파일이 평균을 낮췄을 3개 종목을 언급하지 않았을 뿐입니다.

실제 코호트에서의 동일한 측정

이제 실제 거래 데이터를 바탕으로 동일한 계산을 수행합니다. 아래 패널은 2016년부터 2022년까지 매년 1월을 기준으로 시작 유니버스를 구축합니다. 해당 월 초 가격이 5달러 이상이고 평균 일일 거래량이 25만 주 이상인 심볼을 유지하며, 각 종목의 가장 최근 종가까지 추적합니다. 거래가 중단된 종목은 삭제하지 않고 마지막 종가를 유지합니다. 생존자 열은 현재 거래 중인 심볼만 평균을 냅니다. 전체 유니버스 열은 시작 시점에 존재했던 모든 종목을 평균 냅니다.

조회시작 연도별 생존 종목 평균 대비 전체 코호트 평균 수익률 격차
모든 수치 뒤에 숨겨진 정확한 SQL
WITH
    entry AS
    (
        SELECT
            ticker,
            toYear(date)                   AS cohort_start,
            argMin(toFloat64(close), date) AS entry_close
        FROM global_markets.stocks_daily_aggs
        WHERE toMonth(date) = 1
          AND date BETWEEN '2016-01-01' AND '2022-01-31'
          AND ticker NOT IN ('SPCX')
        GROUP BY ticker, cohort_start
        HAVING argMin(toFloat64(close), date) >= 5
           AND avg(volume) >= 250000
    ),
    outcome AS
    (
        SELECT
            ticker,
            argMax(toFloat64(close), date) AS final_close,
            max(date)                      AS last_bar
        FROM global_markets.stocks_daily_aggs
        WHERE date >= '2016-01-01'
        GROUP BY ticker
    )
SELECT
    toString(e.cohort_start)                                                             AS cohort_year,
    count()                                                                              AS cohort_size,
    countIf(o.last_bar < today() - 45)                                                   AS gone_count,
    round(100 * avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45), 1) AS survivors_only_pct,
    round(100 * avg(o.final_close / e.entry_close - 1), 1)                               AS full_universe_pct,
    round(100 * (avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45)
                 - avg(o.final_close / e.entry_close - 1)), 1)                           AS gap_pct
FROM entry AS e
INNER JOIN outcome AS o ON o.ticker = e.ticker
GROUP BY e.cohort_start
HAVING countIf(o.last_bar >= today() - 45) > 0
ORDER BY e.cohort_start
Run this yourself

2016 코호트의 경우, 이 페이지가 작성될 시점에 2728개 종목 중 968개가 거래를 중단했습니다. 생존자만 평균을 내면 212%이 나옵니다. 전체 코호트를 평균 내면 149.3%이 나옵니다. 격차 열은 두 수치 사이의 거리를 나타내며, 해당 코호트에서는 62.7포인트, 멤버 이탈 시간이 훨씬 짧았던 2022 코호트에서는 9.1포인트입니다. 이는 서로 다른 기간의 총수익률이므로 행 간의 수준을 비교하기보다 격차 열을 아래로 읽어야 합니다. 동일한 유니버스 문제는 월간 수익률 측정 방식과 모든 동일 가중 평균의 기저에 깔려 있습니다.

사라진 종목들의 행방

상장 폐지가 곧바로 전액 손실을 의미하는 것은 아닙니다. 아래 패널은 2019년 1월의 시작 유니버스를 기준으로 이후 어떤 일이 발생했는지 분류합니다.

조회2019년 1월 기준 유니버스 종목별 성과 분류
모든 수치 뒤에 숨겨진 정확한 SQL
WITH
    entry AS
    (
        SELECT
            ticker,
            argMin(toFloat64(close), date) AS entry_close
        FROM global_markets.stocks_daily_aggs
        WHERE date BETWEEN '2019-01-01' AND '2019-01-31'
          AND ticker NOT IN ('SPCX')
        GROUP BY ticker
        HAVING argMin(toFloat64(close), date) >= 5
           AND avg(volume) >= 250000
    ),
    outcome AS
    (
        SELECT
            ticker,
            argMax(toFloat64(close), date) AS final_close,
            max(date)                      AS last_bar
        FROM global_markets.stocks_daily_aggs
        WHERE date >= '2019-01-01'
        GROUP BY ticker
    )
SELECT
    if(o.last_bar >= today() - 45,
       'Still trading',
       concat('Last bar in ', toString(toYear(o.last_bar))))            AS outcome_label,
    count()                                                             AS cohort_size,
    round(100 * avg(o.final_close / e.entry_close - 1), 1)              AS avg_return_pct,
    round(100 * countIf(o.final_close > e.entry_close) / count(), 1)    AS share_above_entry_pct
FROM entry AS e
INNER JOIN outcome AS o ON o.ticker = e.ticker
GROUP BY outcome_label
ORDER BY outcome_label
Run this yourself

Still trading 그룹은 1954개의 종목을 포함하며, 2019년 1월 진입 가격 대비 평균 139.8%의 수익률을 보였고, 그중 72.1%는 시작 가격보다 높은 상태입니다. 9 행을 보면 퇴출 결과는 혼재되어 있습니다. 프리미엄을 받고 인수된 기업은 높은 최종 가격으로 거래를 마칠 수 있는 반면, 상장 폐지 통보를 향해 서서히 하락한 기업은 낮은 가격으로 마감합니다. 둘 다 생존자 전용 파일에서는 누락됩니다. 이들을 다시 포함하면 평균은 변합니다.

생존자 편향 vs 미래 예측 편향

이 두 가지는 함께 언급되지만 백테스트를 망치는 지점은 다릅니다. 미래 예측 편향(look ahead bias)은 타이밍 문제입니다. 테스트가 재작성된 실적 수치나 몇 달 뒤에 적용된 조정치 등 당시에는 아무도 알 수 없었던 사실을 사용하는 경우입니다. 생존자 편향은 구성원 문제입니다. 테스트가 거래하는 명단이 당시 존재했던 명단이 아니라 현재 존재하는 명단이기 때문입니다. 백테스트는 타이밍 면에서 완벽하더라도 사후 확신을 가지고 구성된 유니버스를 거래할 수 있습니다. 가격 조정 또한 관련 함정을 내포하는데, 분할 조정된 가격 이력은 분할일 이전의 모든 바(bar) 수준을 다시 작성하기 때문입니다.

백테스트에서 생존자 편향을 수정하는 방법

  1. 각 날짜별로 유니버스를 재구축하십시오. 시점별(point in time) 구성이란 테스트가 진행되는 날짜에 존재했던 기록을 바탕으로 해당 날짜의 명단을 재구성하는 것을 의미합니다. 테스트는 이후 사라진 종목을 포함하여 당시 거래 가능했던 종목들을 보유하게 됩니다.
  2. 증권 마스터(security master)를 유지하십시오. 증권 마스터는 식별자를 시간에 따라 매핑합니다. 어떤 심볼이 어떤 기간 동안 어떤 기업을 가리켰는지, 인수합병이나 사명 변경 후 무엇으로 대체되었는지를 기록합니다. 가격 파일은 심볼을 기준으로 하지만, 기업은 그렇지 않습니다.
  3. 상장 폐지된 종목은 마지막 실제 가격으로 마감하십시오. 종목이 거래를 중단하면 평균에서 사라지는 대신 마지막 종가로 포지션을 청산해야 합니다. 삭제가 바로 위 패널들에서 격차를 만드는 원인입니다. 재현 가능한 백테스트는 해당 규칙을 코드 내 유니버스 옆에 명시합니다.
  4. 제공업체의 상장 폐지 종목 관련 문서를 읽으십시오. 이를 중요하게 다루는 업체는 기록을 남깁니다. Massive는 티커 엔드포인트를 통해 활성 상태 플래그와 함께 상장 폐지된 심볼을 노출하며, 심볼 변경, 상장 폐지, 인수합병 기록을 별도의 티커 이벤트 엔드포인트로 제공합니다.
"당사는 데이터를 변경하거나 티커 변경 사항을 하나의 집계 시리즈로 연결하지 않습니다."
Massive 지식 베이스, "Massive는 티커 변경 및 인수를 어떻게 처리하는가?", 2026년 8월 열람.

제공업체의 문서에 상장 폐지 종목에 대한 언급이 없다면, 확인하기 전까지는 해당 데이터를 생존자 전용으로 간주하십시오. 소스마다 커버리지 차이가 크므로 무료 주식 시장 데이터 API에 대한 당사의 노트를 참조하십시오.

재활용 티커의 함정

상장이 종료된 후 심볼은 다시 유통될 수 있습니다. 아래 패널은 최소 200개의 일별 바를 기록하고, 최소 250일간 거래가 없다가 2022년 이후 새로운 상장 종목으로 다시 나타난 심볼들을 집계합니다.

조회장기 공백 후 신규 발행사로 재상장된 종목
모든 수치 뒤에 숨겨진 정확한 SQL
SELECT
    toString(toYear(relisted_on)) AS year,
    count()                       AS relisting_count,
    round(avg(dark_stretch))      AS avg_dark_stretch,
    max(dark_stretch)             AS longest_dark_stretch
FROM
(
    SELECT
        ticker,
        any(relist_date)                             AS relisted_on,
        dateDiff('day', max(date), any(relist_date)) AS dark_stretch
    FROM
    (
        SELECT
            d.ticker      AS ticker,
            d.date        AS date,
            i.relist_date AS relist_date
        FROM global_markets.stocks_daily_aggs AS d
        INNER JOIN
        (
            SELECT
                ticker,
                min(toDate(listing_date)) AS relist_date
            FROM global_markets.stocks_ipos
            WHERE toDate(listing_date) BETWEEN '2022-01-01' AND today()
              AND ticker NOT IN ('SPCX')
            GROUP BY ticker
        ) AS i ON i.ticker = d.ticker
        WHERE d.date < i.relist_date
    )
    GROUP BY ticker
    HAVING dateDiff('day', max(date), any(relist_date)) >= 250
       AND count() >= 200
)
GROUP BY year
ORDER BY year
Run this yourself

2022년에는 16개의 심볼이 평균 3247일의 공백기를 거쳐 이런 방식으로 돌아왔습니다. 해당 행에서 가장 긴 공백기는 5734일이었습니다. 심볼만을 기준으로 한 가격 파일은 한 기업의 이력을 다른 기업의 이력에 덧붙이며, 연결은 전체적으로 깔끔해 보입니다. 이는 증권 마스터가 잡아낼 수 있지만 단순 티커 열로는 불가능한 사례입니다.

데이터 노트 및 정의

심볼의 최신 일별 바가 이 페이지가 생성된 날로부터 45일 이내에 있으면 거래 중인 것으로 간주합니다. 이 버퍼는 데이터 앞단의 1~2일 지연과 거래가 드문 종목의 세션 누락을 고려한 것입니다.

수익률은 배당을 제외하고 연율화하지 않은, 1월 진입 종가부터 해당 심볼의 마지막 종가까지의 총 가격 수익률입니다. 초기 코호트는 복리 효과를 누릴 시간이 길었으므로 행 간의 수준은 비교할 수 없습니다. 두 평균 사이의 격차는 비교 가능합니다.

진입 유니버스는 진입 월 동안 가격이 5달러 이상이고 평균 일일 거래량이 25만 주 이상인 심볼을 유지합니다. 일별 거래 데이터에는 운영 기업 외에도 펀드, 유닛, 워런트가 포함되어 있으며 여기서는 이를 구분하지 않으므로 펀드 청산도 기업 상장 폐지와 동일하게 계산됩니다. 공급업체 피드에서 식별자 충돌이 알려진 심볼은 사후 필터링이 아닌 SQL 단계에서 모든 패널에서 제외됩니다.

FAQ

주식 백테스트에서 생존자 편향이란 무엇인가?

오늘날 존재하는 심볼들로만 거래 가능 유니버스를 구축할 때 발생하는 오류입니다. 상장 폐지되거나 인수된 기업들은 테스트에 포함되지 않으므로, 전략은 현재까지 살아남은 종목들만을 대상으로 평가받게 됩니다.

내 데이터셋에 생존자 편향이 있는지 어떻게 알 수 있는가?

몇 년 전 날짜를 기준으로 파일이 커버한다고 주장하는 유니버스를 가져와 각 심볼의 마지막 바를 확인하십시오. 2016년 리스트의 모든 종목이 오늘날에도 가격을 기록하고 있다면, 그 리스트는 현재 상장 종목들로만 구축된 것이며 죽은 종목들은 처음부터 포함되지 않은 것입니다.

생존자 편향은 항상 백테스트 결과를 더 좋게 만드는가?

아닙니다. 방향은 종목들이 어떻게 시장에서 사라졌는지에 따라 달라집니다. 인수는 프리미엄을 받고 마감될 수 있으며, 이러한 종목을 제외하면 코호트 수익률이 과소평가될 수 있습니다. 반면 상장 폐지 통보를 향해 서서히 하락하는 경우는 반대 효과를 냅니다. 위 패널들은 실제 코호트에서 이 두 가지의 순효과를 측정합니다.

시점별(point in time) 유니버스란 무엇인가?

테스트의 각 날짜를 기준으로 재구성된 멤버십 리스트이며, 해당 날짜에 존재했던 기록만을 사용합니다. 당시 거래 가능했으나 이후 사라진 종목은 포함하고, 아직 상장되지 않은 종목은 제외합니다.

생존자 편향은 미래 예측 편향과 같은 것인가?

아닙니다. 미래 예측 편향은 타이밍에 관한 것입니다. 테스트가 아무도 정보를 갖기 전에 정보를 사용하는 경우입니다. 생존자 편향은 구성원에 관한 것입니다. 테스트가 사후 확신을 가지고 구성된 명단을 거래하는 경우입니다. 백테스트는 한쪽은 깨끗할지라도 다른 쪽은 망가질 수 있습니다.


여기에 포함된 모든 패널은 이를 생성한 SQL을 담고 있습니다. 진입 월을 변경하거나 유동성 필터를 완화한 뒤 Strasmore 터미널에서 실행하여 본인의 유니버스 규칙에서 격차가 얼마나 발생하는지 확인하십시오.