퀀트 팩터가 정말 유효한가? Bucket Spread와 Rank IC로 다중 기간 검증
종목 랭킹이나 점수 모델을 만들 때 가장 쉬운 일은 팩터를 하나 더 추가하는 것입니다.
모멘텀, RSI, 상대강도, 52주 신고가와의 거리, 변동성, 거래량. 백테스트에서 조금이라도 관계가 있어 보이면 열 하나를 더 만들고 가중치를 주는 것은 어렵지 않습니다. 진짜 어려운 질문은 그 다음입니다. 이 팩터가 실제로 미래 수익률의 순위를 구분하는가, 아니면 특정 구간에서 우연히 좋아 보였는가?
제 퀀트 리서치 흐름을 정리하면서 예전에는 승률이나 단일 forward-return horizon 숫자에 너무 쉽게 끌렸다는 점을 다시 확인했습니다. 5일 뒤에는 좋아 보이는 팩터가 1일, 3일 뒤에도 같은 방향이라는 보장은 없습니다. 같은 신호를 매일 평가하면 표본 행 수는 빠르게 늘지만 forward return이 크게 겹치기 때문에, 행 수만큼 독립적인 정보가 늘어났다고 보기 어렵습니다.
그래서 내부 노트에 남아 있던 과거 IC나 spread 숫자를 그대로 쓰지 않고 작은 공개 재현 실험을 다시 만들었습니다. 데이터는 공개 저장소의 5개 ETF 조정 종가입니다: sector_prices.csv. 파일은 blob SHA 0a442a9260c2ad64e6cd8c01af49328eb7619367로 고정했고, SPY, XLK, XLF, XLE, XLV에서 몇 가지 단순한 20일 가격 팩터를 만든 뒤 1D / 3D / 5D forward return을 비교했습니다.
결과는 깔끔한 “최고의 팩터”가 아니었습니다. 오히려 그 점이 더 유용했습니다.
20일 모멘텀은 희소 앵커 표본에서 1D와 5D High-Low median spread가 양수였지만 3D에서는 약간 음수로 바뀌었습니다. 저변동성 팩터도 horizon마다 방향이 안정적이지 않았습니다. 더 흥미로운 것은 “SPY 대비 20일 상대강도”를 만든 뒤, 원래 20일 모멘텀과 횡단면 순위가 완전히 같다는 사실을 발견한 것입니다.
이런 불규칙함이 모든 칸이 초록색인 팩터 리포트보다 실제 연구에 더 가깝습니다.
신호가 발생한 시점의 팩터 값을 고정한다
팩터 검증에서 지금 가장 중요하게 보는 것은 공식보다 시간 경계입니다.
어떤 날 시스템이 여러 종목에 점수를 매기고 momentum_20d를 기록했다고 합시다. 실제로 보존해야 하는 것은 그 의사결정 시점에 시스템이 본 값입니다. 그리고 1D, 3D, 5D outcome이 정산된 뒤 그 값이 순위 정보를 가졌는지 확인해야 합니다.
즉:
decision / snapshot -> factor value at t -> realized return at t + N
3일 뒤에 오늘의 데이터와 오늘의 코드로 “그 당시 팩터”를 다시 계산하는 방식이 아닙니다.
둘은 비슷해 보이지만 의미는 꽤 다릅니다. 데이터 공급자가 과거 값을 수정할 수 있고, 배당·분할 조정 방식이 바뀔 수 있으며, 유니버스와 팩터 정의도 달라질 수 있습니다. 횡단면 표준화에 쓰인 종목 집합도 당시와 지금이 다를 수 있습니다. 사후 재계산을 허용하면 “시스템이 실제로 본 과거” 대신 “오늘의 정의로 다시 만든 과거”를 검증하게 될 가능성이 있습니다.
Alphalens도 기본 구조는 비슷합니다. factor value와 이후 forward returns를 맞춘 뒤 quantile별 수익률과 Information Coefficient를 계산합니다. 문서에는 아주 구체적인 look-ahead 위험도 적혀 있습니다. 미래 수익률 자체로 z-score를 계산해 이상치를 제거하면, 데이터 정리 단계에서 이미 미래 정보를 쓴 셈이 됩니다.
나중에 제 프로덕션 리서치 시스템이 자동으로 가중치를 조정한다면 이 경계는 일반 연구 notebook보다 더 엄격하게 두고 싶습니다. factor snapshot이 outcome tracking에 들어간 뒤에는 다음 모델 버전이 과거 값을 덮어쓰지 못하게 만드는 식입니다.
이번 공개 재현은 프로덕션 DB를 사용하지 않았습니다. 하나의 고정된 과거 가격 파일에서 signal-time factor와 forward return을 만들었습니다. 계산 흐름을 검증하기에는 충분하지만, 실제 프로덕션 시스템에 point-in-time 데이터 문제가 없다는 증거는 아닙니다. 두 주장은 분리해야 합니다.
Bucket Spread와 Rank IC는 서로 다른 질문을 본다
팩터를 주로 랭킹에 쓰는 경우라면 저는 처음부터 “승률이 몇 퍼센트인가”를 묻는 방식을 선호하지 않습니다.
승률은 너무 많은 정보를 하나의 Boolean으로 압축합니다. +0.01%와 +8%가 모두 승리이고, 1등과 5등도 같은 승리로 잡힐 수 있습니다. 전략 결과를 묘사하는 데는 유용하지만 “팩터 값이 높을수록 미래 결과도 전반적으로 좋은가”를 직접 답하지는 못합니다.
이번에는 두 가지 기본 지표를 사용했습니다.
첫 번째는 Bucket Spread입니다. 각 signal date마다 횡단면 팩터 값을 순위로 정렬해 Low / Mid / High 세 그룹으로 나눈 다음, High와 Low 그룹의 이후 수익률 차이를 봅니다.
Bucket Spread = median(return_high) - median(return_low)
“높을수록 좋은” 팩터에 실제 순위 정보가 있다면 High가 Low보다 강하기를 기대할 수 있습니다. 하지만 spread가 양수라는 사실은 이 표본에서 양 끝이 벌어졌다는 뜻일 뿐입니다. 통계적 유의성을 자동으로 의미하지도 않고, 거래 비용을 이길 수 있다는 보장도 없으며, 중간 bucket까지 단조롭게 정렬된다는 뜻도 아닙니다.
두 번째는 Rank IC입니다. Spearman rank correlation으로 팩터 순위와 미래 수익률 순위를 직접 비교합니다.
Rank IC = Spearman(factor_rank, forward_return_rank)
SciPy의 spearmanr는 -1에서 1 사이의 비모수 단조 연관성 지표입니다. 많은 팩터에서는 팩터 값과 미래 수익률이 선형 관계인지보다 “순위 자체에 정보가 있는지”가 더 중요하므로 Spearman이 자연스럽습니다.
이번 실험의 핵심 계산은 의도적으로 작게 만들었습니다.
mom20 = px / px.shift(20) - 1
fwd = px.shift(-horizon) / px - 1
x = mom20.loc[dt].dropna()
y = fwd.loc[dt].dropna()
common = x.index.intersection(y.index)
bucket = pd.qcut(
x.loc[common].rank(method="first"),
3,
labels=["low", "mid", "high"],
)
rho, _ = spearmanr(
x.loc[common].values,
y.loc[common].values,
)
spread = (
y.loc[common][bucket == "high"].median()
- y.loc[common][bucket == "low"].median()
)
실제 실험 스크립트는 Content Lab 증거로 고정되어 있고, 실행 명령은 exit code 0으로 끝났습니다. 새로운 퀀트 프레임워크를 만들려는 것이 아니라, 팩터 검증의 핵심을 복잡한 전략 로직에 묻히지 않게 최소 단위로 줄였습니다.
Horizon을 바꾸면 결론도 움직이기 시작한다
forward return 중첩을 조금 줄이기 위해 주요 결과는 5거래일마다 하나의 anchor date를 사용하는 방식으로 표시했습니다. 총 14개 날짜, 날짜당 5개 ETF라서 factor × horizon 조합마다 70개의 횡단면 관측치가 있습니다.
아래 spread는 High bucket median - Low bucket median이고 단위는 percentage points입니다. Rank IC는 각 anchor date의 횡단면 Spearman IC 평균입니다.
| Factor | Horizon | N | Median Spread | Mean Rank IC |
|---|---|---|---|---|
| 20D Momentum | 1D | 70 | +0.583 pp | +0.179 |
| 20D Momentum | 3D | 70 | -0.071 pp | +0.014 |
| 20D Momentum | 5D | 70 | +0.667 pp | +0.214 |
| Distance to 20D High | 1D | 70 | +0.249 pp | +0.089 |
| Distance to 20D High | 3D | 70 | +0.259 pp | +0.151 |
| Distance to 20D High | 5D | 70 | +0.529 pp | +0.252 |
| Low Volatility 20D | 1D | 70 | -0.029 pp | +0.029 |
| Low Volatility 20D | 3D | 70 | +0.296 pp | +0.014 |
| Low Volatility 20D | 5D | 70 | -0.224 pp | -0.207 |
| Relative Strength 20D vs SPY | 1D | 70 | +0.583 pp | +0.179 |
| Relative Strength 20D vs SPY | 3D | 70 | -0.071 pp | +0.014 |
| Relative Strength 20D vs SPY | 5D | 70 | +0.667 pp | +0.214 |
이 표에서 가장 하기 싫은 일은 가장 큰 숫자에 동그라미를 치고 “유효하다”고 선언하는 것입니다.
20D Momentum만 봐도 5D에서는 spread +0.667 pp, mean Rank IC +0.214라서 가중치를 높이고 싶어집니다. 하지만 3D spread는 -0.071 pp이고 Rank IC도 거의 0입니다. “horizon이 길수록 더 좋다”는 깔끔한 구조가 아닙니다.
Low Volatility는 더 불안정합니다. 3D median spread는 양수지만 5D에서는 -0.224 pp로 바뀌고 mean Rank IC도 -0.207입니다. 3D만 잘라 보면 계속 연구할 만한 양의 팩터처럼 보이지만 5D에서는 이야기가 달라집니다.
이것이 multi-horizon outcome tracking의 실용적인 가치입니다. 1D, 3D, 5D를 가중해서 더 예쁜 종합 점수를 만드는 것이 아니라, 먼저 결론이 horizon에 얼마나 민감한지 드러내는 것입니다.
매일 샘플을 잡는 결과도 남겼습니다. 그 버전은 66개의 signal dates와 330개의 factor/outcome 행이 있어서 표본 수가 훨씬 커 보입니다.
문제는 3D, 5D forward returns가 크게 겹친다는 점입니다.
실제로 결과도 달라졌습니다. 20D Momentum의 3D median spread는 daily-overlapping 표본에서 약 +0.578 pp였지만, 5거래일마다 잡은 앵커 표본에서는 -0.071 pp였습니다.
이 차이로 어느 쪽이 “진실”이라고 결론 내리지는 않습니다. 14개의 앵커도 여전히 작고, 5일 간격이 모든 의존성을 제거하는 것도 아닙니다. 더 중요한 메시지는 다른 데 있습니다. 합리적인 샘플링 방식 하나만 바꿔도 팩터 결론이 뒤집힌다면, 리서치 보고서가 큰 총 행 수 하나만 보여 줘서는 안 됩니다.
팩터를 더 늘리기 전에 같은 순위 정보를 반복하는지 확인한다
이번에는 처음에 코드 버그라고 생각한 결과도 있었습니다.
동시에 만든 두 팩터는 다음과 같습니다.
momentum_20d
relative_strength_20d_vs_spy
두 번째는:
asset_20d_return - SPY_20d_return
입니다.
분석을 끝내고 보니 모든 bucket, spread, Rank IC 결과가 완전히 같았습니다.
코드 문제는 아니었습니다.
같은 날짜에서 SPY의 20일 수익률은 모든 자산에 동일한 상수입니다.
RS_i = Momentum_i - C_t
횡단면의 모든 자산에서 같은 C_t를 빼도 순위는 바뀌지 않습니다. Bucket과 Rank IC가 횡단면 rank에만 의존한다면 이 구성에서는 두 정의가 같은 순위 정보를 담습니다.
저에게는 이 사실이 “IC = 0.21” 같은 숫자보다 더 유용했습니다.
멀티팩터 모델은 아주 쉽게 가짜 복잡성을 쌓습니다. 이름과 공식이 달라 보여도 실제로는 같은 방향 정보를 중복 표현할 수 있습니다. 매우 중복된 두 팩터에 독립 가중치를 주는 것은 증거가 두 개 생기는 것이 아니라, 같은 증거에 두 표를 주는 셈입니다.
물론 “relative strength는 momentum과 같다”는 일반 명제는 아닙니다. 업종 대비, 동적 benchmark, 잔차 수익률, rolling beta 조정, 다른 lookback을 쓰면 순위는 달라질 수 있습니다. 이번 동등성은 아주 특정한 구성, 즉 같은 날짜의 모든 자산에서 동일한 SPY 20D return을 뺀 경우에만 해당합니다.
그래서 가중치를 조정하기 전에 factor rank, 상관관계, bucket assignment를 먼저 비교하는 습관을 가져가려 합니다. 새 팩터가 실제로 새로운 정보를 추가하는지, 아니면 이름만 추가하는지 보는 것입니다.
한 번의 양의 IC로 라이브 가중치를 바꾸지는 않는다
연구 시스템이 언젠가 모델을 자동 조정한다면 “팩터를 발견했다”와 “프로덕션에 영향을 준다” 사이에 거리를 두는 편이 낫다고 생각합니다.
후보 팩터는 먼저 observe에 넣습니다. 신호 시점 값은 기록하지만 의사결정에는 참여시키지 않습니다. outcome이 정산되면 여러 horizon의 bucket spread, Rank IC, coverage, 표본 분포를 봅니다.
여전히 흥미롭다면 shadow로 옮깁니다. Shadow는 “활성화한다면 가중치가 어떻게 달라지는가”라는 후보 결과를 만들 수 있지만 active model을 바꾸지는 않습니다.
최소한 다음은 확인하고 싶습니다.
- 표본 수와 non-null coverage가 충분한가;
- High / Mid / Low에 기본적인 순위 구조가 있는가, 아니면 양 끝만 우연히 벌어진 것인가;
- 1D / 3D / 5D 방향을 설명할 수 있는가, 서로 충돌하는가;
- 다른 기간이나 다른 regime에서도 방향이 남는가;
- 기존 팩터와 강하게 중복되는가;
- 샘플링이나 중복 제거 규칙을 바꾸면 결론이 뒤집히는가;
- 연구 과정에서 몇 개의 팩터, window, 파라미터를 시도했는가.
마지막 항목은 특히 놓치기 쉽습니다. 팩터 연구는 많은 가설 검정으로 변하기 쉽습니다. 50개 지표, 6개 lookback, 4개 horizon을 시험한 뒤 가장 예쁜 결과만 고르면, 실제 신호가 약해도 “좋은 결과”가 나올 확률은 올라갑니다. Harvey, Liu, Zhu는 자산가격 연구의 다중검정 문제를 … and the Cross-Section of Expected Returns에서 직접 다룹니다.
그래서 Bucket Spread와 Rank IC를 팩터 입장권으로 보지 않습니다. 기본 검진에 가깝습니다. 순위 정보가 있는지, 방향이 안정적인지, horizon flip이나 팩터 중복이 있는지를 빠르게 보여 줍니다. 하지만 “실제 돈으로 거래할 수 있는 팩터”까지 가려면 안정성, 거래 비용, out-of-sample 검증, regime 의존성, 포트폴리오 노출, 리스크 관리가 더 남아 있습니다.
이번 공개 실험도 5개 ETF와 14개의 희소 앵커 날짜만 사용했습니다. 어떤 프로덕션 alpha도 증명하지 않습니다. 증명한 것은 검증 체인을 작은 형태로 재현할 수 있다는 것, 그리고 horizon·샘플링·rank redundancy를 드러내기만 해도 깔끔해 보였던 팩터 결론이 훨씬 덜 확실해진다는 점입니다.
저는 그 불확실성을 남겨 두는 편이 낫다고 봅니다.
모델에 들어갈 가치가 있는 팩터라면 한 번의 연구 보고서에서 예쁜 숫자가 나왔기 때문이 아니라, 오랜 outcome tracking과 여러 불편한 검사를 거친 뒤에도 살아남았기 때문이어야 합니다.