교정할 때마다 수익이 줄었다 — 백테스트 CAGR 6.4% → 5.9% → 1.0%

백테스트 오염은 상장폐지된 종목의 데이터가 깨진 채로 남아, 그 결함이 전략 수익률에 섞여 드는 문제다. 같은 전략의 같은 구간을 데이터가 정직해지는 방향으로 두 번 교정하자 연 수익률이 6.4%에서 5.9%로, 다시 1.0%로 내려앉았다. 하락의 대부분인 4.9%p는 흔히 지목되는 생존편향이 아니라 상장폐지 종목의 데이터 품질에서 왔다.

교정 두 번에 백테스트 CAGR이 6.4%에서 1.0%로 내려앉다

3구역 타임라인 도해 — 왼쪽 '죽은 회사' 패널의 하락 물결선, 가운데 '11년의 공백' 점선 상자 속 달력 눈금, 오른쪽 '다른 상품' 패널의 ETF 배지와 상승 물결선을 붉은 실선 하나가 끊기지 않고 관통하고, 공백 아래에 '한 종목으로 이어 붙음' 칩이 달려 있다

2007년에 시작하는 20년 가까운 미국 주식 데이터로 추세추종 전략 하나를 돌렸다. 오르는 흐름을 따라 사고, 그 흐름이 꺾이면 파는 규칙이다. 그 규칙을 과거 데이터에 그대로 적용해 성과를 재는 일을 백테스트라고 한다. 결과는 연 6.4%였다. 규칙 하나가 그 긴 구간을 버틴 기록으로는 그럴듯해 보였다.

그 6.4%는 결국 1.0%가 됐다. 그사이 전략 코드는 한 줄도 바뀌지 않았다. 다음에 한 일이 전략을 고치는 것이 아니라 데이터를 정직하게 만드는 교정 두 번이었기 때문이다. 첫 번째는 종목 후보군을 그 시점에 실제로 지수에 들어 있던 목록으로 되돌리는 것이다.

두 번째는 깨진 상폐 데이터를 걸러내는 가드다.

세 숫자를 나란히 적으면 이렇다. 같은 전략의 연평균 복리 수익률(CAGR — 총수익을 매년 같은 비율로 불어난 셈 치고 연 단위로 환산한 값)은 6.4%에서 5.9%로, 다시 1.0%로 내려앉았다. 코드도 전략도 기간도 손대지 않은 채 나온 세 값이다.

6.4%에서 5.9%로 0.5%p, 다시 1.0%로 4.9%p가 빠지는 폭포 막대 그림. 두 번째 낙차가 첫 번째보다 열 배 가까이 길다
그림 1. 전략 A의 CAGR 3단계. 첫 교정은 0.5%p, 두 번째 교정은 4.9%p를 가져간다.

낙차는 고르게 나뉘지 않았다그림 1. 첫 교정이 가져간 몫은 0.5%p, 두 번째가 가져간 몫은 4.9%p다. 부풀림의 원인으로 가장 먼저 지목되는 것은 생존편향인데, 이 원장에서 그 몫은 전체 하락의 열에 하나였다. 나머지 아홉은 다른 데서 왔다.

생존편향을 고쳤더니 겨우 0.5%p였다

생존편향은 살아남은 것만 보고 판단하는 오류다. 백테스트에는 종목 후보군을 고르는 순간 들어온다. 지금 지수에 들어 있는 목록을 과거 구간에도 그대로 쓰면, 그사이 망해서 지수에서 빠진 회사는 애초에 후보에 없다. 망한 종목이 없는 세계에서 성과를 재는 셈이다.

첫 감사가 잡은 것이 정확히 이 결함이었다. 게이트 다섯 중 유일한 실패가 생존편향이었다 — 종목 후보군이 위키피디아의 ‘현재’ S&P 500 구성종목이었고, 그 목록을 전 기간에 적용하고 있었다. 20년 가까운 구간 전체의 성과를 2026년의 명단으로 재고 있었다는 뜻이다. 이 글에 나오는 수익률은 아카이브의 것이든 아래 합성 재현의 것이든 전부 같은 식으로 잰다.

(1) $$ \mathrm{CAGR} = \left(\frac{V_{\mathrm{end}}}{V_{\mathrm{start}}}\right)^{1/n} – 1 $$

식 1에서 $n$은 햇수이고, 아래 합성 재현에서는 10이다. 출력에 찍히는 숫자도 전부 이 식을 통과한 값이라, 10년 총수익만 알면 손으로 검산할 수 있다.

원장 자체는 유료 데이터와 비공개 코드로 돌아가 그대로 따라 돌릴 수 없다. 그래서 같은 기전을 합성 데이터로 다시 만들었다. 종목 400개를 10년치로 세우고 전부 30달러에서 출발시킨 뒤, 하루 수익률에는 정규분포에 가끔 터지는 폭락을 섞었다.

시작가에서 85% 떨어지면 그날 상폐되고 가격은 그 자리에서 멈춘다. 시드 다섯 개는 결과를 보기 전에 정해 뒀다. 아래 코드가 하는 일은 같은 세계를 두 번 재는 것이다. 한 번은 10년을 살아남은 종목만으로, 한 번은 상폐된 종목까지 전부 넣고.

import numpy as np

DAYS, N = 2520, 400              # 10년(252거래일×10), 종목 400개
SEEDS = [11, 12, 13, 14, 15]     # 결과를 보고 고른 시드가 아니다 — 미리 정해 뒀다

def universe(seed):
    """합성 유니버스: 일별 로그수익률 + 가끔 터지는 하루짜리 폭락. 85% 하락하면 상폐."""
    rng = np.random.default_rng(seed)
    mu = rng.normal(0.00015, 0.0005, size=N)
    r = rng.normal(mu[:, None], 0.018, size=(N, DAYS))
    crash = rng.random((N, DAYS)) < 0.0015
    r = r + crash * np.log(1 - rng.uniform(0.10, 0.35, size=(N, DAYS)))
    p = 30.0 * np.exp(np.cumsum(r, axis=1))
    below = p < 4.5                                   # 시작가 30의 15%
    dead = np.where(below.any(axis=1), below.argmax(axis=1), DAYS)
    for i in range(N):                                # 상폐 후에는 그 가격에서 멈춘다
        if dead[i] < DAYS:
            p[i, dead[i]:] = p[i, dead[i]]
    return p, dead

def cagr(total):
    return (total ** (1 / 10) - 1) * 100

gaps = []
for s in SEEDS:
    p, dead = universe(s)
    alive = dead == DAYS
    surv = cagr(np.mean(p[alive, -1] / p[alive, 0]))   # 지금 살아 있는 종목만
    full = cagr(np.mean(p[:, -1] / p[:, 0]))           # 상폐 포함 전 종목
    gaps.append(surv - full)
    print(f"seed {s}: 생존자만 {surv:5.2f}%  전체 {full:5.2f}%  갭 {surv - full:+.2f}%p"
          f"  (상폐 {N - alive.sum()}/{N})")
print(f"갭 중앙값 {np.median(gaps):+.2f}%p")
seed 11: 생존자만  9.59%  전체  7.30%  갭 +2.29%p  (상폐 81/400)
seed 12: 생존자만  9.39%  전체  6.56%  갭 +2.83%p  (상폐 98/400)
seed 13: 생존자만 11.23%  전체  8.22%  갭 +3.01%p  (상폐 101/400)
seed 14: 생존자만  9.98%  전체  6.92%  갭 +3.05%p  (상폐 104/400)
seed 15: 생존자만  9.87%  전체  7.12%  갭 +2.75%p  (상폐 95/400)
갭 중앙값 +2.83%p

살아남은 종목만 골라 보면 다섯 시드 전부에서 연 2.3~3.1%p가 공짜로 붙는다. 다만 이 숫자를 앞의 0.5%p와 나란히 놓으면 안 된다. 합성 재현이 잰 것은 종목 후보군 선택 하나가 만드는 갭이고, 0.5%p는 전략 A가 그 교정에서 실제로 받은 타격이다. 재는 대상이 다르니 두 숫자는 크기를 겨룰 수 없다.

질문은 그대로 남는다. 생존편향이 0.5%p뿐이었다면 나머지 4.9%p는 어디서 왔나.

진짜 전쟁 — 상장폐지 데이터의 세 가지 함정

원장에 남긴 한 문장은 이렇다. 생존편향 교정은 멤버십 문제가 아니라 상폐 데이터 품질 전쟁이다. 지수에서 빠진 종목을 후보군에 되돌리는 순간, 그 종목들의 데이터 상태가 성과를 좌우하기 시작한다. 살아 있는 종목의 시계열은 대체로 멀쩡하다. 죽은 종목의 것은 그렇지 않다.

세 가지 함정이 실측됐고, 셋의 성격이 서로 다르다. 첫째는 빈 응답이다. 데이터 제공자가 아무것도 돌려주지 않고, 지표 계산이 첫 원소를 집는 자리에서 죽는다. 여섯 종목이 여기 걸려 제외됐다. 시끄럽게 죽는 종류라 알아채기는 쉽다.

둘째는 티커(종목마다 붙는 짧은 종목 기호) 재사용이다. 죽은 회사가 쓰던 티커를 몇 년 뒤 다른 상품이 물려받는다. JAVA가 그 예다 — 썬 마이크로시스템즈의 티커였다가 JP모건(JPM)의 상장지수펀드(ETF)로 넘어갔고, 두 시계열 사이에 4270일, 11년이 넘는 공백이 있다. 로더가 둘을 한 종목으로 이어 붙이면 그 공백은 캔들(하루치 시가·고가·저가·종가를 한 칸에 그린 막대) 사이에 숨는다. 없던 트레이드가 거기서 생겨난다. 열일곱 종목에서 갭 절단이 걸렸고, JAVA 하나가 만들어 낸 유령 수익이 +253R이었다.

셋째는 복귀형 스파이크다. 수정계수(배당·분할을 반영해 과거 가격을 보정하는 배수)가 오염돼 가격이 191달러에서 1.58달러로 떨어졌다가 184달러로 되돌아온다(CBE). 열네 종목이 격리됐다. 가격 자체가 이상해 보이는 종류라 눈에 띈다.

셋을 한 성격으로 묶으면 판단이 흐려진다. 첫째는 크래시로 시끄럽게 드러나고, 셋째는 가격만 봐도 이상하다. 조용한 것은 둘째뿐이다. 가격은 멀쩡한데 종목이 바뀌어 있다. 아카이브는 셋을 나란히 적을 뿐 위험도를 매기지 않는다 — 둘째가 가장 위험하다는 것은 내 판단이다.

이 감사의 1차 판정은 FAIL이었다. 생존편향 한 건 때문에 멈췄고, 같은 날 저녁 시점별 멤버십으로 종목 후보군을 되돌린 뒤 PASS(공시 조건부)로 재판정됐다 — 잔여 한계를 공시한 채로 통과했다는 뜻이다. 이 원장은 FAIL, FAIL(power), NULL, INCONCLUSIVE, BLOCKED를 서로 다른 판정으로 나눠 보관한다. "쟀고 통과 못 했다"와 "검정력이 없어 재지 못했다"는 다른 사건이고, 가설을 소진시키는 쪽은 앞엣것뿐이다.

합성 재현 ① 티커 재사용 — 죽은 회사가 산 것처럼 보인다

티커 재사용이 성과에 어떻게 들어오는지도 합성으로 세워 볼 수 있다. 앞의 합성 종목 후보군에 종목 하나를 더한다. 3년에 걸쳐 40달러에서 3달러로 죽고, 4년을 쉬고, 다른 상품이 55달러에서 재개하는 종목이다. 공백을 따지지 않는 나이브 로더는 그 4년을 마지막 값 3달러로 채운 뒤 재개일에 이어 붙인다. 그날 하루 수익률이 +1,733%로 찍힌다.

전략은 저가 매수로 잡았다. 21거래일마다 종가 5달러 미만인 종목을 전부 같은 비중으로 사서 한 달 보유하는 규칙이다. 죽은 회사의 3달러에 매달 들어가니 점프가 터지는 날 반드시 그 자리에 있다. 가드는 그 종목의 갭 구간부터 신규 매수를 막는다. 원장에서 갭 절단의 임계값은 365일이다. 볼 것은 두 숫자다 — 가드를 껐을 때의 10년 성적과 켰을 때의 10년 성적.

import numpy as np

DAYS, N = 2520, 400
SEEDS = [11, 12, 13, 14, 15]

def universe(seed):
    rng = np.random.default_rng(seed)
    mu = rng.normal(0.00015, 0.0005, size=N)
    r = rng.normal(mu[:, None], 0.018, size=(N, DAYS))
    crash = rng.random((N, DAYS)) < 0.0015
    r = r + crash * np.log(1 - rng.uniform(0.10, 0.35, size=(N, DAYS)))
    p = 30.0 * np.exp(np.cumsum(r, axis=1))
    below = p < 4.5
    dead = np.where(below.any(axis=1), below.argmax(axis=1), DAYS)
    for i in range(N):
        if dead[i] < DAYS:
            p[i, dead[i]:] = p[i, dead[i]]
    return rng, p

def splice(rng):
    """3년에 걸쳐 40→3으로 죽고, 4년 공백 뒤 다른 상품이 55에서 재개한다."""
    s = np.empty(DAYS)
    s[:756] = np.geomspace(40, 3, 756)          # 3년 하락
    s[756:1764] = 3.0                            # 공백 — 나이브 로더는 마지막 값으로 채운다
    s[1764:] = 55.0 * np.exp(np.cumsum(rng.normal(0.0004, 0.012, DAYS - 1764)))
    return s

def backtest(prices, ban=None):
    """저가 매수: 21거래일마다 종가 5달러 미만 전부 동일가중, 한 달 보유."""
    equity = 1.0
    for t in range(252, DAYS - 21, 21):
        cheap = np.where(prices[:, t] < 5.0)[0]
        if ban is not None and t >= ban[1]:       # 가드: 갭 이후 그 종목 신규 매수 금지
            cheap = cheap[cheap != ban[0]]
        if len(cheap):
            equity *= float(np.mean(prices[cheap, t + 21] / prices[cheap, t]))
    return (equity ** (1 / 10) - 1) * 100

naives, guards = [], []
for s in SEEDS:
    rng, p = universe(s)
    uni = np.vstack([p, splice(rng)])
    naive, guarded = backtest(uni), backtest(uni, ban=(N, 756))
    naives.append(naive); guards.append(guarded)
    print(f"seed {s}: 나이브 {naive:+.2f}%  가드 {guarded:+.2f}%  차이 {naive - guarded:+.2f}%p")
print(f"중앙값: 나이브 {np.median(naives):+.2f}%  가드 {np.median(guards):+.2f}%")
print(f"스플라이스 재개일 하루 수익률 {(55 / 3 - 1) * 100:+.0f}%")
seed 11: 나이브 +1.47%  가드 -1.51%  차이 +2.98%p
seed 12: 나이브 +0.47%  가드 -2.21%  차이 +2.68%p
seed 13: 나이브 +1.20%  가드 -1.49%  차이 +2.69%p
seed 14: 나이브 -0.32%  가드 -2.86%  차이 +2.54%p
seed 15: 나이브 -0.92%  가드 -3.61%  차이 +2.70%p
중앙값: 나이브 +0.47%  가드 -2.21%
스플라이스 재개일 하루 수익률 +1733%
나이브 로더와 갭 절단 가드의 자산 곡선 두 개. 7년째 재개 시점에서 나이브 곡선만 수직으로 튀어 오르고, 가드 곡선은 그 지점을 지나쳐 완만하게 내려간다
그림 2. 종목 401개 중 단 하나가 갈라놓은 두 궤적. 재개일 하루의 +1,733%가 나이브 쪽 곡선을 통째로 들어 올린다.

가드를 켜면 이 전략의 10년 성적은 연 −2.21%다그림 2. 지는 전략이다. 가드를 끄면 +0.47%가 되어 본전 근처에서 버틴 전략처럼 보인다. 다섯 시드 전부에서 갭은 2.5~3.0%p였고, 그 전부를 401종목 중 하나가 만들었다. 하루짜리 점프 하나가 10년 성적표의 부호를 바꾼다.

합성 재현 ② 복귀형 스파이크 — 트레이드 1건이 없던 우위를 만든다

세 번째 함정도 같은 방식으로 세워 본다. 종목 하나의 가격을 190달러 수준으로 올려놓고, 5년째 되는 날 하루만 1.6달러로 떨어뜨렸다가 다음 날 되돌린다. CBE에서 관찰된 191 → 1.58 → 184와 같은 모양이다.

이번 전략은 역추세다. 하루에 25% 넘게 빠진 종목을 다음 날 사서 5일 들고 있는 규칙이다. 단일 계좌를 순서대로 굴리는 방식은 쓰지 않았다. 대신 이 규칙이 만드는 트레이드 전건을 모아 평균을 냈다.

순차 체결 방식은 다섯 시드 중 셋에서 오염 스파이크를 타이밍 문제로 우연히 비켜 갔기 때문이다. 우연히 피해 간 결과를 성적으로 적으면 함정의 크기를 실제보다 작게 보게 된다. 볼 것은 트레이드 평균 수익이 오염 1건에 얼마나 끌려가는가다.

import numpy as np

DAYS, N = 2520, 400
SEEDS = [11, 12, 13, 14, 15]

def universe(seed):
    rng = np.random.default_rng(seed)
    mu = rng.normal(0.00015, 0.0005, size=N)
    r = rng.normal(mu[:, None], 0.018, size=(N, DAYS))
    crash = rng.random((N, DAYS)) < 0.0015
    r = r + crash * np.log(1 - rng.uniform(0.10, 0.35, size=(N, DAYS)))
    p = 30.0 * np.exp(np.cumsum(r, axis=1))
    below = p < 4.5
    dead = np.where(below.any(axis=1), below.argmax(axis=1), DAYS)
    for i in range(N):
        if dead[i] < DAYS:
            p[i, dead[i]:] = p[i, dead[i]]
    return p

for s in SEEDS:
    p = universe(s)
    i, d = 7, 1260                       # 5년째, 한 종목의 하루만 190대 → 1.6 → 복귀
    p[i] = 190.0 * p[i] / p[i, 0]
    p[i, d] = 1.6
    daily = p[:, 1:] / p[:, :-1] - 1
    rows, cols = np.where(daily[:, : DAYS - 7] <= -0.25)      # 하루 -25% 이상 급락
    rets = p[rows, cols + 6] / p[rows, cols + 1] - 1          # 다음 날 매수, 5일 보유
    bad = (rows == i) & (cols == d - 1)
    print(f"seed {s}: 트레이드 {len(rets)}건  평균 {rets.mean() * 100:+6.1f}%"
          f"  오염 1건 빼면 {rets[~bad].mean() * 100:+.2f}%"
          f"  그 1건 {rets[bad][0] * 100:+,.0f}%")
seed 11: 트레이드 526건  평균  +26.1%  오염 1건 빼면 -0.12%  그 1건 +13,796%
seed 12: 트레이드 498건  평균  +49.6%  오염 1건 빼면 +0.15%  그 1건 +24,603%
seed 13: 트레이드 543건  평균  +13.3%  오염 1건 빼면 +0.12%  그 1건 +7,142%
seed 14: 트레이드 558건  평균  +53.7%  오염 1건 빼면 -0.11%  그 1건 +30,024%
seed 15: 트레이드 543건  평균  +43.4%  오염 1건 빼면 +0.37%  그 1건 +23,367%

500건 안팎의 트레이드 가운데 오염된 것은 하나다. 그 하나를 빼면 이 규칙의 평균 수익은 0% 근처에서 오르내린다 — 돈을 벌어다 주는 구석이 없다는 뜻이다. 하나를 넣으면 트레이드당 평균이 +13~+54%로 뛴다.

오염 포함과 제외의 트레이드 수익 분포 두 개가 0% 근처에서 거의 겹쳐 있고, 평균선만 오염 포함 쪽이 오른쪽으로 크게 밀려 있다. 단일 오염 트레이드는 축 밖에 있다는 주석이 달려 있다
그림 3. 두 분포의 벌크는 사실상 같다. 갈리는 것은 평균선뿐이고, 그 차이를 만든 트레이드 한 건은 그림 밖에 있다.

두 분포를 겹쳐 그리면 사정이 더 분명해진다그림 3. 대다수 트레이드가 모인 자리는 두 쪽이 거의 포개진다. 평균을 옮긴 그 트레이드는 수익률 +13,796%라 그림 밖에 있다. 요약 통계 한 줄만 보고 판단할 때 이런 결함이 정확히 숨는다.

한 가지가 더 있다. 이 재현의 첫 설계는 12-1 모멘텀(최근 한 달을 뺀 12개월 상승률로 종목을 고르는 규칙)이었는데, 스플라이스 점프를 수확하지 못했다. 모멘텀은 점프가 일어난 뒤에야 그 종목을 고르기 때문이다. 저가 매수로 바꾸자 다섯 시드 전부에서 수확됐다. 같은 데이터, 같은 오염인데 전략 구조에 따라 성과에 들어오기도 하고 안 들어오기도 한다. 그래서 판별 질문이 하나 늘어난다 — 내 백테스트는 오염을 수확하는 구조인가.

오염 필터가 생존편향을 뒷문으로 되불러온다

오염을 걸러내는 쪽이 늘 옳아 보이지만, 반대 방향으로 난 함정이 하나 있다. 복귀형 스파이크를 잡는 필터는 가격이 비정상적으로 낮게 찍혔다가 되돌아오는 모양을 찾는다. 그런데 1달러 밑에서 거래되는 종목의 시계열이 원래 그렇게 생겼다. 호가 단위(가격이 움직이는 최소 칸) 하나가 몇십 퍼센트라 정상 거래도 스파이크처럼 보인다.

그래서 필터를 세게 걸면 멀쩡한 저가주가 오염으로 잘못 걸린다. 문턱을 올려 대응하면 이번에는 진짜로 망해서 1달러 아래로 내려간 종목이 함께 지워진다. 망한 종목이 빠진 데이터셋, 그게 바로 앞에서 없앴다고 한 그 생존편향이다.

원장의 방어는 두 겹이다. 하나는 1달러 하한이다 — 그 아래 구간은 오염 판정에서 빼서 멀쩡한 저가주가 걸려드는 것을 막는다. 다른 하나는 멤버-일 커버리지 하드게이트다. 후보군 종목-날짜 가운데 실제 데이터가 확보된 비율이 90% 밑이면 백테스트를 아예 돌리지 않는다. 실측은 94.8%로 통과했다. 같은 게이트가 인접 라인에서는 반대로 작동했다 — 한 라인은 커버리지 89.8%에서 걸려 INCONCLUSIVE로 정지했고, 이 판정은 "우위가 없다"가 아니라 "가드가 발동해 판정을 내리지 않았다"는 뜻이다.

교정 후 수익이 오르면 오염을 의심하라

원장에서 뽑아낸 한 문장은 이것이다. 데이터를 보수적으로 고쳤는데 성과가 좋아졌다면, 고친 것이 아니라 새로 오염된 것이다. 교정은 없던 트레이드에서 나온 수익을 지우는 일이라 대개 성과를 깎는다. 방향이 반대로 나왔다면 먼저 의심할 것은 새로 들어온 결함이다.

자기 데이터에서 세 함정을 찾는 일은 오래 걸리지 않는다. 셋 다 한 번씩 훑는 스캔으로 끝난다.

  1. 종목마다 거래일 사이 간격을 재서 365일(원장 가드의 실제 임계값이다)이 넘는 공백을 전부 뽑는다. 공백 앞뒤의 가격 배율이 몇 배씩 튀면 티커 재사용을 의심하고 그 지점에서 시계열을 자른다.
  2. 하루 수익률이 −80% 아래로 떨어졌다가 며칠 안에 원래 수준으로 되돌아오는 구간을 뽑는다. 수정계수 오염의 모양이다. 단 1달러 미만 구간은 이 스캔에서 빼야 오탐이 줄어든다.
  3. 데이터 응답이 빈 경우를 조용히 건너뛰지 않는다. 그 종목을 목록에서 명시적으로 빼고 몇 종목을 뺐는지 개수를 남긴다. 침묵으로 처리하면 커버리지가 얼마나 줄었는지 알 수 없다.

백테스트를 돌리지 않는 사람에게도 같은 규칙이 옮겨붙는다. 결측을 앞 값으로 채우고, 재사용된 식별자를 합치고, 이상치를 지우는 파이프라인이 전부 같은 모양의 결함을 만든다. 데이터를 정리한 뒤 모델 성능이 올랐다면, 좋아진 것인지 새는 곳이 생긴 것인지 먼저 갈라야 한다.

교정을 마친 1.0%는 자랑할 숫자가 아니다. 그래도 6.4%보다는 믿을 만하다. 검증이 한 일은 딱 그만큼이다 — 자랑거리였던 6.4%를 점검 대상으로 바꿔 놓았다.

이 글은 검증 기록이며, 특정 종목이나 상품의 매수·매도 권유가 아니다.

더 읽기

  • Brown 외, "Survivorship Bias in Performance Studies", The Review of Financial Studies 5(4), 1992.
  • Bailey 외, "Pseudo-Mathematics and Financial Charlatanism", Notices of the AMS 61(5), 2014.
  • fja05680/sp500 — 이 글의 시점별 멤버십 교정에 쓴 S&P 500 구성종목 데이터셋.
  • 본문의 실측·판정은 글쓴이의 비공개 퀀트 연구 원장에서 옮겨 적은 것이다.