미래 참조 편향(Look-Ahead Bias): 한 바(bar)의 실수가 순수한 노이즈에서 Sharpe 15를 만들어내는 방법
"환상 없는 백테스트" 시리즈의 일부입니다.
📄 이 글은 하나의 연구 논문으로 발전했습니다. 세 가지 미묘한 미래 참조 누출을 알려진 실제값(4,000개의 시뮬레이션된 히스토리)에 대해 통제된 테스트로 검증합니다. 논문은 lookahead.marketmaker.cc에서 온라인으로(인터랙티브 버전 + PDF) 읽을 수 있으며, 코드와 데이터는 github.com/suenot/lookahead-inflation에 있습니다.
몇 주 전, 우리의 파라미터 탐색 벤치마크가 우리에게 거짓말을 하고 있었는데, 우리는 그것을 거의 알아채지 못했습니다.
엔진은 깔끔해 보였습니다. 종가 바 로직, 정직한 롤링 워크포워드 분할, 파라미터 공간에 대한 Sobol/QMC 탐색, 홀드아웃 테스트 윈도우까지. 탐색은 인샘플에서 좋아 보이는 구성들을 찾아냈습니다. 유일한 문제는: 아웃오브샘플에서는 거의 모든 것이 마이너스였다는 것입니다. 우리는 전략이 그저 약한 것이라고 생각했습니다.
그러다 한 줄을 발견했습니다. 시그널은 바 i의 종가에서 결정되었지만, 체결은 다음 바의 시가가 아니라 동일한 바 i에 기록되어 있었습니다. 실행 인덱스에서의 하나의 오프바이원(off-by-one) 오류였습니다. 체결을 open[i+1]로 옮기자 — 바 i의 종가를 본 후 실제로 거래할 수 있는 유일한 가격입니다 — 아웃오브샘플 결과의 부호가 뒤집혔습니다. Sobol 탐색은 손실에서 이익으로 바뀌었습니다. 전략에 대해 바뀐 것은 아무것도 없었습니다. 우리는 그저 과거에서 거래하는 것을 멈췄을 뿐입니다.
이것이 바로 미래 참조 편향이며, 불안한 부분은 실수가 얼마나 작았는지와 그 결과가 얼마나 컸는지입니다. 이 글은 통제된 자체 감사입니다: 구조상 실제값을 알고 있는 시뮬레이터를 만들고, 미묘한 누출을 하나씩 주입하며, 각각이 백테스트를 정확히 얼마나 부풀리는지 측정합니다. 핵심 결과: 실제 엣지가 전혀 없는데도, 동일 바 체결은 순수한 노이즈만으로 연율화 Sharpe +14.8을 만들어냅니다.
미래 참조 편향이 실제로 무엇인가

미래 참조 편향은 파이프라인의 어느 지점에서든 결정이나 측정이 그것을 사용하는 시점에 실시간으로는 이용할 수 없었을 정보를 사용하는 것을 말합니다. 교과서적인 예시는 조잡합니다 — 1월에 어떤 주식의 연간 실적 전체를 사용하거나, 아직 발표되지 않은 재작성 자료를 사용하는 식입니다. 이런 것들은 발견하기 쉽습니다. 코드 리뷰에서 살아남는 것들은 미묘하며, 세 곳에 숨어 있습니다:
- 실행 — 바
i에서 결정하고 바i에서 체결합니다 (또는 시그널을 발생시킨 바로 그 바의 고가/저가를 스톱에 사용합니다). 자신을 촉발시킨 것과 상관관계가 있는 가격으로 거래하게 됩니다. - 정규화 — 미래를 포함한 전체 시계열에 대해 계산된 통계량을 사용해 피처를 z-스코어, min-max, 또는 다른 방식으로 스케일링합니다. 스케일러가 테스트 셋을 "알고" 있게 됩니다.
- 지표 / 피처 — 중심화된(또는 다른 방식으로 미래를 엿보는) 윈도우로 평활화하거나 필터링하여, 바
i의 값이 이미 바i+1의 일부를 포함하게 됩니다.
이 세 가지 모두 머신러닝 문헌에서 **누출(leakage)**이라 부르는 것의 형태입니다: 타깃의 미래로부터 온 정보로 훈련/평가가 오염되는 것입니다(Kaufman et al., 2012; Kapoor & Narayanan, 2023). 금융 분야에서 정전(正典)이라 할 만한 다룸은 López de Prado의 Advances in Financial Machine Learning(2018)입니다 — 퍼지드 교차검증, 엠바고, 백테스팅의 위험성. 시점 정합성(point-in-time) 규율은 최소한 Fama & French(1992)까지 거슬러 올라가는데, 이들은 회계 데이터를 의도적으로 6개월 지연시켜 변수가 그것이 설명하는 수익률보다 먼저 알려지도록 했습니다.
이 글이 답하려는 질문은 정량적입니다: "누출이 나쁜가"(모두가 동의하는 것)가 아니라, "각 형태가 얼마나 많은 Sharpe 포인트를 벌어주며, 어떤 것이 위험한가?"입니다. 숫자가 없으면 이에 대해 추론할 수 없습니다. +0.3의 부풀림이 노이즈인지, +14의 부풀림이 결정적 증거인지 구별할 수 없습니다.
실제값을 아는 시뮬레이터

부풀림을 측정하려면 진실을 알아야 합니다. 실제 데이터는 결코 진실을 말해주지 않습니다 — 하나의 실현값만 줄 뿐 오라클은 주지 않습니다. 그래서 우리는 우리가 엣지를 설정하는 합성 시장을 만들었습니다.
데이터 생성 과정은 엄격하게 인과적이며 발산하지 않습니다:
여기서 는 지속성을 갖는 외생적 잠재 드리프트(인 AR(1))이고, 바 수익률 는 한 바 앞서 알려진 작은 드리프트 를 가집니다. 가 과거 수익률에 의존하지 않기 때문에 피드백이 없으며 아무것도 발산하지 않습니다. 파라미터 는 실제 엣지가 얼마나 존재하는지를 조절하는 다이얼입니다:
- — 널(null): 엣지가 전혀 없음. 양의 백테스트 Sharpe는 무엇이든 100% 인공물입니다.
- — 실제로 거래 가능한 엣지: 정직한 모멘텀 규칙이 실제로 돈을 법니다.
전략은 의도적으로 단순합니다 — 모멘텀 부호 규칙입니다. 피처는 후행 구간 수익률의 합( 바)이며, 포지션은 그 부호입니다:
csum = np.concatenate(([0.0], np.cumsum(r))) # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]
signal = np.sign(mom) # position for the next bar
이 모멘텀 피처는 동일 바 누출을 연구하기에 완벽한 수단입니다. 실제 지표들이 공유하는 속성을 갖고 있기 때문입니다: 기계적으로 현재 바를 포함한다는 것입니다. mom[t]는 r[t]를 포함합니다. 따라서 r[t]를 트레이드로 기록한다면, 당신은 부분적으로 이미 자신의 시그널 안에 들어 있는 값에 베팅하고 있는 셈입니다. 이것이 바로 구체화된 누출입니다.
설정: (바당 1% 변동성), 편도 수수료 0.00045(왕복 0.09%, 우리 엔진과 일치), (시간봉)로 연율화된 Sharpe, 각각 4,000 바로 이루어진 4,000개의 독립적인 히스토리. 모든 것이 시드로 고정되어 있고 결정론적입니다.
정직한 파이프라인 (유일하게 거래 가능한 것)
바 t의 종가에서 결정하고, 다음 바의 수익률을 벌며, 포지션 변경에 수수료를 지불합니다:
def sharpe(sig, ret_booked):
dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
pnl = sig * ret_booked - FEE_ONEWAY * dpos
return pnl.mean() / pnl.std() * np.sqrt(8760)
honest = sharpe(signal[idx], r[idx + 1]) # earn r[t+1]: tradable
세 가지 누출, 각각 하나의 정밀한 변경
same_bar = sharpe(signal[idx], r[idx])
z_full = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])
z_sm = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0 # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])
각 누출은 정직한 파이프라인에서 단 한 줄 떨어져 있습니다. 바로 이것이 핵심입니다: 이것들은 특이한 실수가 아니라, 리뷰를 통과할 만한 종류의 것들입니다.
결과: 각 누출의 규모

4,000개의 시드에 걸쳐 실행한 결과, 각 파이프라인이 보고하는 연율화 Sharpe는 널(엣지 없음)과 실제 엣지(, 정직한 Sharpe가 그럴듯한 +1.57이 되도록 조정됨) 조건에서 다음과 같습니다:
| 파이프라인 | 널 (엣지 없음) | 실제 엣지 |
|---|---|---|
| 정직함 (진실) | −0.74 | +1.57 |
| 동일 바 체결 | +14.79 | +15.85 |
| 지표 엿보기 (1바) | +4.76 | +6.62 |
| 전체 시계열 정규화 | −0.84 | +1.46 |
시드 전반에 걸친 95% 신뢰구간은 모든 셀에서 ±0.05 이하이며, 효과가 실재하는 곳에서 부풀림에 대한 대응 t검정은 천문학적으로 유의합니다(t > 400, p ≈ 0).
먼저 널 열을 읽으십시오. 이것이 가능한 한 가장 깨끗한 실험이기 때문입니다: 엣지가 없으므로 정직한 파이프라인은 정확하게 돈을 잃습니다(−0.74, 노이즈를 거래하는 데 드는 수수료의 손실입니다). 이제 누출들이 그 동일한 "아무것도 없음"에 무슨 짓을 하는지 보십시오:
- 동일 바 체결: −0.74 → +14.79. 예측력이 전혀 없이 무작위 노이즈를 거래하는 전략이 거의 15에 가까운 연율화 Sharpe를 보고합니다. 이것은 미묘한 편향이 아니라 조작입니다. 메커니즘은 정확히 우리가 심어놓은 그것입니다: 모멘텀 피처가
r[t]를 포함하므로,r[t]를 기록하는 것은 자신의 시그널에 베팅하는 것입니다. - 지표 엿보기: −0.74 → +4.76. 평활화 필터가 미래로 한 바를 보게 하면, 노이즈로부터 5에 가까운 Sharpe가 만들어집니다.
t에서의 평활화된 값이 이제 곧 벌어들일r[t+1]과 상관관계를 갖기 때문입니다. - 전체 시계열 정규화: −0.74 → −0.84. 사실상 부풀림이 없습니다. 이것은 정직하지만 직관적이지 않은 발견입니다(아래에서 더 다룹니다).
엣지 열은 더 음흉한 메시지를 전달합니다. 실제 엣지가 존재할 때(정직하게는 +1.57), 누출은 단순히 상수를 더하는 것이 아니라 — 측정된 Sharpe를 실제로 거래할 수 있는 +1.57보다 훨씬 높은 +15.85와 +6.62로 밀어올립니다. 그러므로 측정된 수치는 실력과 누출을 구별할 수 없습니다. 누출된 +6과 정직한 +6은 보고서상에서 동일하게 보입니다. 어느 쪽이었는지는 자본을 투입한 뒤에야 알게 됩니다.
누출은 스위치가 아니라 그라데이션이다

자연스러운 반론이 있을 수 있습니다: "시그널 바 전체를 기록하는 것은 극단적이고 비현실적인 실수다." 그래서 우리는 용량(dose) — 누출이 포착하는 시그널 바의 비율 — 을 0(정직함)에서 1(완전한 동일 바)까지 훑어보았습니다:
| 포착 비율 | 널 Sharpe | 엣지 Sharpe |
|---|---|---|
| 0.00 (정직함) | −0.74 | +1.57 |
| 0.25 | +3.90 | +6.41 |
| 0.50 | +9.86 | +12.20 |
| 1.00 (완전한 누출) | +14.79 | +15.85 |
시그널 바의 4분의 1만 포착해도 엣지 없는 전략은 −0.74에서 +3.90으로 올라갑니다. 완전한 오프바이원이 없어도 속을 수 있습니다 — 시그널 바에서 약간 낙관적인 슬리피지, 트리거한 바 자체를 기준으로 확인하는 바 내(intrabar) 스톱 같은, 살짝 지나치게 유리한 체결이면 대부분의 "배포 가능" 임계값을 넘기기에 충분합니다. 부풀림은 현재를 얼마나 거래하도록 자신에게 허용하는지에 대해 매끄럽고 단조롭게 나타납니다.
이것이 손실 전략을 프로덕션에 투입하는 빈도는 얼마나 될까?
실무자를 걱정하게 만들어야 할 수치는 **허위 배포율(false-deployment rate)**입니다: 실제로는 손실을 내는 구성이 배포를 승인하는 데 사용할 기준을 얼마나 자주 통과하게 만드는가입니다. "연율화 Sharpe ≥ 1.0"을 배포 기준으로 사용할 때, 널 조건에서:
- 동일 바 체결: 엣지 없는 전략의 68%가 배포 가능해 보이지만 실제로는 손실을 냅니다. 순수한 노이즈 구성 셋 중 둘은 Sharpe-≥-1 게이트를 통과하고 실전에서 돈을 잃게 됩니다. (여기서 이 비율이 깔끔하게 정의될 수 있는 것은 누출이 순수하게 실행에만 있기 때문입니다 — 정직한 대응물은 동일한 시그널에 정직한 체결을 적용한 것입니다.)
- 지표 엿보기: 이 역시 엣지 없는 구성 거의 전부를 배포 기준 위로 밀어올립니다(99.9%가 Sharpe ≥ 1을 통과) — 노이즈를 그대로 프로덕션으로 통과시켜 버립니다.
- 전체 시계열 정규화: 12%만 기준을 통과합니다 — 사실상 노이즈의 기저율일 뿐, 누출로 인한 프리미엄은 없습니다.
분류 체계와 각각을 탐지하는 방법
세 가지 누출은 위험도가 동등하지 않으며, 그 차이는 시사하는 바가 큽니다.
1. 실행 누출 (비용이 큰 것)

증상: 체결 가격이 시그널과 상관관계를 갖는데, 둘 다 동일한 바에서 나오기 때문입니다. 규모: 막대함(완전한 용량에서 노이즈로부터 +15, 4분의 1 용량에서 +3.9). 왜 최악인가: 시그널은 거의 정의상 최근 가격 움직임으로부터 구성되므로, 시그널 바의 수익률은 정확히 당신의 피처와 가장 상관관계가 높은 대상입니다. 그것을 기록하는 것은 답을 미리 들여다보는 것에 가깝습니다.
탐지 — 1바 시프트 테스트. 이것이 이 글에서 가장 가치 있는 단일 진단법입니다. 백테스트를 가져와서 모든 체결을 한 바 뒤로 미루십시오(i에서 결정, open[i+1]에서 체결). 결과가 거의 변하지 않는다면, 실행은 정직했던 것입니다. 결과가 붕괴하거나 부호가 뒤집힌다면, 당신은 과거에서 거래하고 있었던 것입니다. 이것이 바로 우리의 Sobol 탐색에서 일어난 일입니다: 체결을 시프트하자 "수익성 있던" OOS가 손실로 드러났습니다 — 아니, 더 정확히는 누출이 제거되자 진짜 관계가 드러난 것입니다.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. 지표 / 피처 누출 (조용한 것)
증상: 바 i의 지표가 i+1 이후의 데이터에 의존합니다 — 중심화된 이동평균, 인과적 지연이 없는 필터, 확인을 위해 미래 바가 필요한 고점/저점 라벨, 미래 캔들을 입력받는 Heikin-Ashi 방식 변환. 규모: 큼(노이즈로부터 +4.8). 왜 숨는가: 누출이 라이브러리 호출 안에 파묻혀 있기 때문입니다. scipy.signal.filtfilt는 제로페이즈(zero-phase)이며 — 제로페이즈는 곧 비인과적이라는 뜻입니다. "이 바가 국소 최고점이다"라는 피처는 다음 바가 찍히기 전까지는 알 수 없습니다.
탐지: 모든 지표에 대해 그것이 읽는 가장 높은 인덱스가 무엇인지 물어보십시오. t에서의 값을 계산하는 과정이 조금이라도 t+1을 건드린다면, 그것은 비인과적입니다. 지표를 확장/롤링 인과적 윈도우에서 계산하고, t 이후의 바가 배열에 존재하든 존재하지 않든 바 t에서의 값이 동일한지 검증하십시오. (우리의 HMA/ADX 구현은 이를 통과합니다: t에서의 모든 출력은 ≤ t의 입력만 읽습니다.)
3. 정규화 누출 (채널 특이적인 것)
증상: 스케일러(StandardScaler, min-max, 전역 z-스코어)가 테스트 셋을 포함한 전체 데이터셋에 대해 피팅됩니다. 정전이라 할 만한 ML 경고들은 이에 대해 명시적입니다 — Hastie, Tibshirani & Friedman의 Elements of Statistical Learning §7.10.2("교차검증을 하는 잘못된 방법과 올바른 방법"), 그리고 scikit-learn 자체의 흔한 함정 가이드: "평균은 전체 데이터의 평균이 아니라 훈련 서브셋의 평균이어야 한다."
우리 테스트에서의 규모: ≈ 제로(−0.74 → −0.84). 이것은 놀랍지만 정직한 결과이며, 암기하기보다는 이해할 가치가 있습니다.
왜 부풀지 않았을까요? 우리 전략이 피처를 오직 그 부호(제로 임계값)를 통해서만 사용하기 때문입니다. 표준편차 스케일링은 부호를 절대 바꾸지 않으며, 전역 평균 중심화는 제로 크로싱을 살짝 움직일 뿐입니다. 그래서 순수한 부호 규칙에 대한 전체 시계열 표준화는 거의 무해합니다.
이것을 과도하게 일반화하지 마십시오. 정규화 누출은 채널 특이적입니다. 전략이 피처의 크기를 사용하는 순간 — z-스코어에 비례하는 포지션 사이징, 스케일된 분포를 보고 선택한 0이 아닌 진입 임계값, 표준화된 입력을 받는 신경망 — 미래를 아는 스케일러가 중요해지기 시작하며, 전역 통계량이 인과적 통계량과 다를수록 더 중요해집니다. 우리의 결과는 "정규화 누출은 안전하다"가 아닙니다. "누출의 규모는 누출된 값이 결정에 진입하는 채널에 달려 있으며, 가정하지 말고 측정해야 한다"는 것입니다. 부호 규칙은 이 특정 누출이 저렴한 하나의 사례일 뿐입니다.
이것이 연결되는 지점
미래 참조 편향은 이 시리즈가 기록해 온 연쇄에서 첫 번째 고리입니다:
- 그것은 검증의 입력을 오염시킵니다. 누출된 백테스트는 워크포워드 분할을 무난히 통과하고, 과적합 피크가 아니라 넓은 플래토처럼 보입니다 — 누출이 폴드 전반에 걸쳐 일관적이므로 교차검증이 이를 잡아낼 수 없습니다. 누출은 과적합보다 상류에 있는 실패 모드이며, 하류에서 아무리 정직하게 검증해도 구해줄 수 없습니다.
- 그것은 파라미터 탐색과 상호작용합니다: 누출된 데이터에 대한 수천 번의 시도에 걸친 탐색은 누출을 가장 공격적으로 활용하는 구성을 찾아냅니다. "승자"가 가장 나쁜 범죄자입니다.
- 이것이 백테스트-라이브 패리티가 어긋나는 이유입니다. 백테스트와 봇 사이의 30-50% 격차에 대해 누출이 가장 깔끔한 설명입니다. 라이브 트레이딩은 기계적으로 미래를 엿볼 수 없는 유일한 장소이기 때문입니다.
이 모든 것을 잡아내는 규율은 학계 문헌이 수년간 촉구해 온 것과 동일합니다: 백테스트를 엄격한 정보 경계를 가진 통계적 실험으로 다루는 것입니다. Bailey, Borwein, López de Prado & Zhu는 과적합이 얼마나 쉽게 가짜 성과를 만들어내는지 보여주었고(2014), Arnott, Harvey & Markowitz의 백테스팅 프로토콜(2019)은 그 위생 수칙을 성문화합니다. 미래 참조 편향은 그중에서도 가장 기본적인 경계 — 시간의 경계 — 이며, 실수로 위반하기 가장 쉬운 것입니다.
핵심 요점

- 미래 참조 편향은 정량적으로는 거대하고 정성적으로는 보이지 않습니다. 단 하나의 1바 실행 오류가 Sharpe −0.74(순수한 노이즈, 정확히 손실 중)를 +14.79로 바꾸었습니다. 실수는 한 줄이지만, 그 결과는 조작된 트랙 레코드입니다.
- 그것은 그라데이션입니다. 시그널 바의 25%만 포착해도 아무것도 없는 상태에서 +3.90이 나옵니다. 노골적인 버그가 필요한 것이 아닙니다 — 체결에 대한 약간의 지나친 낙관만으로 충분합니다.
- 측정된 수치는 실력과 누출을 구별할 수 없습니다. 실제 엣지가 존재할 때, 누출은 실제로 거래 가능한 진실을 훨씬 넘어서까지 보고서를 부풀립니다. 유일한 방어는 지표가 아니라 프로세스입니다.
- 1바 시프트 테스트가 가장 빠른 진단법입니다. 모든 체결을 한 바 뒤로 미루십시오. 성과가 붕괴한다면, 당신은 과거에서 거래하고 있었던 것입니다.
- 누출의 규모는 채널 특이적입니다. 실행과 지표 엿보기는 치명적이지만, 부호 규칙에 대한 전체 시계열 정규화는 거의 공짜입니다. 누출이 실제로 진입하는 채널을 통해 측정하십시오 — 가정하지 마십시오.
전체 통제된 연구 — 세 가지 누출 모두, 용량 스윕, 허위 배포 분석, 정식 방법론, 그리고 단일 결정론적 스크립트로 재현 가능한 모든 수치 — 는 lookahead.marketmaker.cc의 동반 논문에 있으며, 코드와 데이터는 github.com/suenot/lookahead-inflation에 있습니다.
우리의 널 실험에서 전략은 엣지가 전혀 없었습니다. 그런데도 Sharpe 15를 보여주었습니다. 백테스트가 너무 좋아 보인다면, 가장 먼저 의심해야 할 것은 당신의 천재성이 아니라 — 당신의 시계입니다.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.