충실도 게이트(Fidelity Gate): 저비용 프록시가 고비용 평가와 같은 순위를 매기지 않는 한, 성긴 것에서 정밀한 것으로(Coarse-to-Fine) 진행하는 백테스팅은 당신을 더 빨리 속인다
"환상 없는 백테스트" 시리즈의 일부.
진지한 파라미터 탐색은 모두 같은 벽에 부딪힌다. 탐색 공간은 방대하고, 제대로 된 평가 하나하나는 비용이 크다. 십여 개의 임계값과 세 개의 지표 기간을 가진 멀티 타임프레임 전략을 다중 폴드 워크포워드(walk-forward) 분할로 평가하면, 설정 하나당 몇 초가 걸릴 수 있다. 만 개의 설정이면 몇 시간이다. 그래서 지름길을 찾게 되는데, 그 지름길은 항상 같은 발상이다. 저렴하게 평가하고, 살아남은 것만 비용이 큰 테스트로 승격시킨다. 만 개의 설정을 폴드 하나로 걸러내고, 상위 몇백 개만 남긴 뒤, 그것들만 모든 폴드에 대해 실행한다. 시간봉 캔들로 걸러내고, 최종 후보들에 대해서만 1초 단위로 드릴다운한다.
이것이 바로 성긴 것에서 정밀한 것으로 가는(coarse-to-fine) 탐색이다 — 드릴다운, 다중 충실도, successive halving, Hyperband, ASHA — 그리고 이는 최적화 분야에서 정말로 훌륭한 발상 중 하나다. 하루 걸릴 연산을 한 시간으로 줄일 수 있다. 우리도 이를 사용한다. 하지만 여기에는 이를 도입한 대부분의 사람들이 결코 확인하지 않을 만큼 조용한 함정이 숨어 있다. 그 함정은 이것이다. 이 방법 전체는 당신의 저렴한 프록시가 비용이 큰 평가와 동일한 방식으로 설정들의 순위를 매긴다고 가정한다. 만약 그렇지 않다면, 당신은 더 빠르게 탐색하는 게 아니다. 첫 번째 관문에서 미래의 승자를 버리고 노이즈를 승격시키고 있는 것이다 — 그저 스스로를 더 빨리 속이고 있을 뿐이다.
이 글은 당신의 드릴다운이 실재하는 것인지 아니면 착각인지를 말해주는 단 하나의 측정값에 관한 것이다. 우리는 이를 우리 자신의 멀티 타임프레임 탐색에 적용해 보았고, 그 답은 불편했다. 가장 저렴한 충실도에서 우리의 프록시는 설정들의 순위를 거의 무작위로(스피어만 ρ ≈ 0.03) 매겼다. 만약 그 충실도에서 공격적으로 가지치기(pruning)를 했더라면 — 기성 ASHA의 기본값들이 대개 그렇게 하도록 유도하듯이 — 우리는 최초 라운드에서 최종 승자들을 버렸을 것이다. 해결책은 탐색을 시작하기 전에 실행하는 필수 게이트다. 순위 상관관계를 측정하고, 그것이 신뢰할 만한 충실도 아래에서는 가지치기를 거부하는 것이다.
다중 충실도 탐색이 실제로 하는 일

이 부류의 방법들은 모두 하나의 형태를 갖는다. 먼저 충실도(fidelity)(자원(resource) 또는 *예산(budget)*이라고도 부른다)를 정의한다. 이는 저렴한 값에서 비싼 값까지 조절 가능한 다이얼로, 최댓값에서는 진짜 목적함수 값을 준다. 그런 다음 낮은 충실도에서 설정들을 실행하고, 순위를 매기고, 상위 일부만 남긴 뒤, 그 생존자들만 더 높은 충실도에서 다시 실행한다 — 소수의 최종 후보가 전체 충실도에서 평가될 때까지 이를 반복한다.
핵심은 successive halving(Jamieson & Talwalkar, 2016)이다. N개의 설정을 최소 자원에서 시작해, 상위 1/η만 남기고, 자원을 η배로 늘린 뒤 이를 반복한다. Hyperband(Li et al., 2018)는 successive halving을 외부 루프로 감싸 서로 다른 시작 자원들에 걸쳐 위험을 분산시킴으로써, 얼마나 공격적으로 가지치기를 해야 할지 추측할 필요가 없게 해준다. ASHA(Li et al., 2020)는 이를 비동기적이고 병렬 처리에 적합하게 만든 버전이다. 제거율 η는 이들이 공유하는 유일한 조절값이며, 표준적인 기본값은 η = 3이다 — 각 단계마다 상위 3분의 1만 남기고 예산을 세 배로 늘린다.
충실도 자체는 저렴한 것에서 비싼 것으로 이어지며 결국 진실에 수렴하는 것이라면 거의 무엇이든 될 수 있다.
- 워크포워드 폴드의 개수. 폴드 1개, 그다음 2개, 3개, 이렇게 K개까지 평가한다. 이는 아래에서 우리가 다룰 충실도인데, 롤링 방식의 표본외(out-of-sample) 분할을 사용하는 백테스트에서 자연스러운 선택이기 때문이다.
- 캔들 해상도. 1시간봉으로 걸러내고, 1분봉으로 승격시키고, 최종 후보들에 대해서만 1초 단위 또는 원시 체결(raw trades) 데이터까지 드릴다운한다. 이는 체결 시뮬레이션이 아니라 탐색에 적용한 적응형 드릴다운 발상이다.
- 히스토리 길이, 에폭(epoch), 데이터셋 서브샘플 — 전통적인 ML 충실도들이다.
백테스트에서는 폴드와 해상도, 이 둘이 중요한데 — 이것이 이 글 전체의 요점이다 — 이 둘은 똑같이 안전하지 않다. 둘 중 하나는 아무리 신중하게 게이트를 걸어도 조용히 잘못된 파라미터를 선택하게 만드는 편향을 갖고 있다.
모든 것이 의존하는 단 하나의 가정

successive halving이 올바르게 작동하기 위해 실제로 필요한 것이 무엇인지 적어보자. 저렴한 충실도가 올바른 목적함수 값을 낼 필요는 없다 — 폴드 1개가 폴드 6개와 다른 샤프 지수를 보고한다고 해서 신경 쓸 사람은 아무도 없다. 필요한 것은 더 약하지만 훨씬 더 구체적인 조건이다. 저렴한 충실도는 비용이 큰 충실도와 동일한 방식으로 설정들의 순위를 매겨야 한다는 것이다. 설정 A가 전체 충실도에서 설정 B를 이긴다면, A는 낮은 충실도에서도 B를 이기는 경향이 있어야 한다. 그것이 전부다. 그리고 그것이 곧 모든 것이다.
형식적으로 말하면, 드릴다운이 작동하는지 여부를 좌우하는 양은 설정 공간 전체에 걸친 저충실도 목적함수와 고충실도 목적함수 사이의 순위 상관관계다. 그 상관관계가 높다면, 저렴한 라운드의 생존자들은 비용이 큰 라운드에서도 승리했을 바로 그 설정들이며, 당신은 연산을 공짜로 절약한 것이다. 만약 낮다면, 저렴한 라운드는 무작위 필터에 불과하다. 좋은 설정을 버리고 나쁜 설정을 승격시키며, 당신은 더 나쁜 답으로 빠르게 가기 위해 예산을 써버린 것이다. 당신을 구해줄 η의 중간 설정 같은 것은 없다 — 나쁜 프록시는 가지치기를 완만하게 해도 여전히 승자를 흘리고, 공격적으로 가지치기하면 아예 출혈을 일으킨다.
이 실패는 늘 그렇듯 눈에 보이지 않는다. 탐색은 완료되고, 챔피언을 보고하며, 그 챔피언은 표본내(in-sample)에서 멀쩡해 보인다. 어떤 오류도 발생하지 않는다. 프록시가 거짓말을 하고 있었다는 사실은 챔피언이 표본외에서 무너질 때에야 발견되는데, 그때는 이미 그 실패의 원인을 전략 탓으로 돌리고, 그것을 선택한 탐색 방법 탓으로는 돌리지 않은 뒤다. 그래서 원칙은 이 시리즈가 계속해서 도달하는 것과 같다. 방법을 신뢰하기 전에, 그 방법이 가정하는 것을 측정하라. 드릴다운의 경우, 측정해야 할 것은 순위 상관관계이며, 그 측정은 저렴하다.
측정하기: 충실도 게이트

게이트는 본격적인 탐색 전에, 앞으로 탐색할 바로 그 공간에서 단 한 번 실행하는 작은 실험이다. 절차는 다음과 같다.
- 파라미터 공간에서 몇백 개의 설정을 무작위로 뽑는다(우리는 약 200개를 사용한다 — 안정적인 스피어만 추정치를 얻기에 충분하면서도 감당할 만큼 저렴하다).
- 각 설정을 모든 충실도 단계에서 평가한다. 폴드 1개, 2개, … 전체 K개까지. 폴드 개수 충실도의 경우 이는 거의 공짜에 가까운데, 저렴한 단계에서 계산한 폴드들이 비용이 큰 단계에서도 재사용되는 누적 평균이기 때문이다.
- 각 단계
r에 대해, 그 약 200개 설정 전체에 걸쳐 r-폴드 순위와 전체 K-폴드 순위 사이의 **스피어만 순위 상관관계(Spearman rank correlation)**를 계산한다. - ρ가 임계값을 넘는 첫 번째 단계(우리는 ρ ≥ 0.5를 사용한다)가 가지치기를 허용할 수 있는 가장 얕은 충실도다. 그보다 아래에서는 순위가 신뢰하기에 너무 시끄러우므로, 그 지점에서 설정을 제거해서는 안 된다.
전체 코드는 스무 줄 남짓이다. 그 핵심은 다음과 같다.
def fidelity_check(cache, n_probe, seed=7):
"""Spearman ρ: cumulative mean over the first r folds (in FOLD_ORDER)
vs the full K-fold objective, on n_probe random configs."""
rng = np.random.default_rng(seed)
k = len(FOLDS)
per_fold = np.empty((k, n_probe))
order_win = np.array([list(FOLDS[fi]) for fi in FOLD_ORDER], np.int64)
for j in range(n_probe):
p = _unit_to_params(rng.random(len(PNAMES))) # random config
scores, *_ = eval_group(cache, p, _sp_of(p)[None, :], order_win, False)
per_fold[:, j] = scores[0] # score on each fold
cums = np.cumsum(per_fold, axis=0) / np.arange(1, k + 1)[:, None] # r-fold mean
rhos = []
for r in range(1, k):
rho = spearmanr(cums[r - 1], cums[-1]).statistic # r folds vs all K
rhos.append(0.0 if math.isnan(rho) else rho)
return rhos
한 가지 의도적인 세부사항에 주목하라. 폴드는 FOLD_ORDER라는 순서로 취해지는데, 이는 캘린더의 이른 구간과 늦은 구간을 번갈아 가며 배치하는 인터리브(interleaved) 순서다(폴드 0, 그다음 중간 폴드, 그다음 폴드 1, 그다음 더 나중 폴드, …). 이는 대단히 중요하며 이후 절의 주제이기도 하다. 즉 "폴드 1개"란 역사의 중간 부분에 걸쳐 있는 하나의 구간을 뜻하고, "폴드 2개"란 이른 구간 하나와 늦은 구간 하나를 뜻한다 — 결코 최근의 연속된 구간이 아니다. 저렴한 충실도는 더 적은 폴드를 사용함으로써 저렴해지는 것이지, 더 최근의 폴드를 사용함으로써 저렴해지는 것이 아니다.
결과: 폴드 하나로는 거의 무작위로 순위가 매겨진다

우리의 멀티 타임프레임 설정 두 가지에 대해 실행한 게이트가 보고한 결과는 다음과 같다. 이 숫자는 약 200개의 무작위 설정에 걸친 r-폴드 순위와 전체 K-폴드 순위 사이의 스피어만 ρ이다 — 저충실도 순위가 전체 충실도 순위를 얼마나 충실하게 예측하는지를 나타낸다.
폴드 r |
멀티 TF 실행 | 더 어려운 레짐 |
|---|---|---|
| 1 | +0.43 | +0.03 |
| 2 | +0.67 | +0.43 |
| 3 | +0.78 | +0.78 |
| 4 | +0.82 | — |
| 5 | +0.91 | +0.91 |
먼저 '더 어려운 레짐' 열을 읽어보자. 이것이 경각심을 불러일으키는 쪽이기 때문이다. 폴드 1개에서 ρ = 0.03이다. 이는 약한 상관관계가 아니라 상관관계가 전혀 없는 것이다 — 200개 설정에 대한 폴드 1개짜리 순위는 통계적으로 그것들을 무작위로 섞은 것과 구별되지 않는다. 대부분이 그렇듯 기본값으로 min_resource = 1에서 가지치기를 시작하도록 구성된 successive-halving 실행은, 이 레짐에서 첫 번째이자 가장 공격적인 컷을 동전 던지기로 하게 되는 셈이다. 전체 충실도에서 결국 승리할 설정들은 바로 그 첫 라운드에서 살아남을 확률이 반반이다. 순위가 어떤 의미를 갖기 시작하는 충실도(폴드 3개에서 ρ = 0.78)에 도달할 무렵에는, 대부분이 이미 사라진 뒤다.
멀티 TF 열은 더 완만하지만 같은 논점을 다른 방식으로 보여준다. 그곳에서도 폴드 1개는 ρ = 0.43을 주는데 — 이는 우리의 0.5 게이트보다 낮다. 꽤 괜찮은 상관관계처럼 보이는데, 바로 그것이 위험한 지점이다. 0.43은 당신을 안심시키기에는 충분히 높고, 최고의 설정들을 흘려보내기에도 충분히 낮다. 폴드 2개(ρ = 0.67)에 이르러서야 순위가 신뢰할 만해진다.
여기서 두 가지를 일반화할 수 있다. 첫째, 폴드 1개에서의 ρ는 레짐에 따라 달라지고 신뢰할 수 없다 — 우리는 여러 설정에 걸쳐 0.03에서 0.43까지 측정했고, 어느 경우에도 폴드 하나만으로는 기준을 넘지 못했다. 둘째, ρ는 단조롭게, 그리고 빠르게 상승한다. 폴드 3개에 이르면 두 설정 모두 0.78이 되고, 5개에 이르면 0.91로 수렴한다. 신호는 분명히 존재한다. 다만 그것을 보려면 충분한 충실도를 들여야 할 뿐이다. 게이트의 역할은 "충분함"이 시작되는 정확한 단계를 찾아내고, 그 아래에서는 가지치기를 금지하는 것이다.
폴드 하나가 그토록 시끄러운 이유
이 노이즈는 우리의 폴드 구성 방식이 가진 버그가 아니다. 이는 본질적인 것이며, 그 이유를 이해하면 이를 잘못된 방식으로 "고치려" 드는 일을 막을 수 있다. 단일 워크포워드 폴드는 짧은 구간 — 하나의 시장 레짐에 해당하는 몇 주 — 이다. 그 구간에서 전략의 점수는 그 파라미터가 우연히 그 레짐에 얼마나 잘 들어맞았는가에 지배되며, 이는 그것들이 얼마나 잘 일반화되는지와는 느슨하게만 관련되어 있다. 진짜로 품질이 다른 두 설정도, 그저 그중 하나가 그 폴드에 우연히 들어 있던 추세를 잡아냈다는 이유만으로 단일 폴드에서 순위가 뒤바뀔 수 있다. 폴드 하나에서의 목적함수 값은 당신이 실제로 관심 있는 목적함수에 대한 분산이 큰 추정량이며, 순위 상관관계야말로 큰 분산이 가장 먼저 파괴하는 것이다 — 추정량의 평균은 보존하면서도 그 순위는 순전한 노이즈일 수 있다.
폴드를 추가하면 이 분산이 평균을 통해 줄어든다. 각 폴드는 시장 상황에 대한 부분적으로 독립적인 표본이며, r-폴드 평균은 분산이 더 낮은 추정량이고, 그 순위는 전체 충실도 순위로 수렴해 간다. 이것이 바로 ρ = 0.03 → 0.43 → 0.78 → 0.91로 상승하는 이유다. 목적함수 자체가 바뀌는 것이 아니라, 레짐 특유의 운이 평균을 통해 상쇄되면서 순위 추정치가 안정화되는 것이다. 여기서 얻는 교훈은 백테스트에서 충실도란 근본적으로 얼마나 많은 독립적인 레짐을 표본으로 삼았는가에 관한 것이라는 점이다 — 그리고 하나만으로는 순위를 매기기에 거의 항상 충분하지 않다.
이는 또한 왜 더 어려운 레짐은 0.03에서 시작하고 멀티 TF 실행은 0.43에서 시작하는지도 설명해준다. 더 어려운 레짐에서는 단일 폴드가 더 레짐 특이적이다 — 설정들의 폴드 1개짜리 점수가 지속적인 우위보다는 운에 의해 더 많이 좌우되기 때문에, 순위가 무작위에 더 가까워진다. 게이트는 그 차이를 자동으로 읽어내고, 가지치기를 하기 전에 더 많은 폴드를 요구함으로써 대응한다. 당신이 지금 어떤 레짐에 있는지 미리 알 필요는 없다. 그저 측정하면 된다.
코드로 본 게이트: 최소 충실도 자동 상향 조정
게이트의 출력은 단일 정수, 즉 ASHA가 가지치기를 하도록 허용된 가장 얕은 충실도인 min_resource다. 규칙은 기계적이다 — 각 단계를 순서대로 살펴보며, ρ가 임계값을 넘는 첫 번째 단계를 취한다.
RHO_GATE = 0.5
min_res = len(FOLDS) # default: pruning OFF (full fidelity)
rhos = fidelity_check(cache, n_probe=200) # [ρ@1, ρ@2, …, ρ@(K-1)]
passing = [r for r, rho in enumerate(rhos, 1) if rho >= RHO_GATE]
if passing:
min_res = passing[0] # first rung that clears the gate
pruner = SuccessiveHalvingPruner(min_resource=min_res, reduction_factor=3)
이를 두 실행에 대해 추적해 보자. 멀티 TF 설정에서는 ρ@1 = 0.43이 게이트를 통과하지 못하지만 ρ@2 = 0.67은 통과하므로, min_resource는 2로 자동 상향된다. ASHA는 무엇이든 제거하기 전에 모든 설정을 최소 두 폴드에서 실행하고, 그 지점부터 정상적으로 가지치기를 한다. 더 어려운 레짐에서는 ρ@1 = 0.03과 ρ@2 = 0.43이 둘 다 실패하고, ρ@3 = 0.78이 처음으로 통과하므로 min_resource는 3이 된다. 그리고 결정적인 대비책이 있다. 만약 어떤 단계도 0.5에 도달하지 못하면, passing은 비어 있고 min_resource는 K로 유지된다 — 가지치기가 완전히 꺼지며, 탐색은 빠르지만 틀린 방식이 아니라 평범한 전체 충실도 탐색으로 우아하게 격하된다. 자신의 프록시를 증명하지 못하는 드릴다운은 그저 가지치기를 거부한다.
이것이 제어 흐름 한 줄에 담긴 철학 전체다. 모든 successive-halving 라이브러리의 기본값은 "min_resource = 1부터 가지치기하고, 저렴한 단계를 신뢰하라"이다. 게이트는 이를 "데이터가 신뢰할 만하다고 말하는 첫 번째 단계부터 가지치기하고, 그런 단계가 없다면 가지치기를 하지 마라"로 대체한다. 이는 사전에 약 200개 설정짜리 탐침(probe) 하나만큼의 비용이 들 뿐이며, 드릴다운을 믿음의 행위에서 측정된 결정으로 바꾸어준다. 제거율 η = 3은 그대로 유지된다. 게이트는 당신이 가지치기를 얼마나 세게 하는지는 건드리지 않고, 오직 얼마나 일찍 시작할 수 있는지만 건드린다.
위 코드에서 드러나는 정직한 사실 하나가 있다. min_resource를 높이면 속도 향상 폭이 줄어든다는 것이다. 폴드 1개 대신 3개에서 가지치기한다는 것은 버려질 설정들을 포함한 모든 설정이 폴드 3개만큼의 비용을 치른다는 뜻이다. 그것이 정확성의 대가이며, 이는 올바른 거래다. 진짜 승자를 찾아내는 탐색에서의 2배 더 적은 속도 향상이, 그것들을 버리는 탐색에서의 6배 속도 향상보다 낫다. 게이트는 이 거래를 숨기지 않고 명시적으로 드러낸다.
잘못된 저렴한 축: 짧은 히스토리라는 함정
유혹적이고, 뻔하고, 편향된 저렴한 충실도가 하나 있다. 모두가 가장 먼저 손을 뻗는 것이기에 이름을 붙여 짚고 넘어갈 가치가 있다. 바로 더 짧은 히스토리다. 지난 한 달치로 설정들을 걸러내고, 생존자들을 전체 2년치로 승격시킨다. 이는 사소할 정도로 저렴하며 폴드 수를 줄이는 것과 같은 발상처럼 느껴진다. 하지만 그렇지 않다.
더 적은 폴드와 더 짧은 히스토리는 결정적인 지점에서 다르다. 폴드 개수 충실도는 제대로 구성하면 여전히 전체 캘린더를 아우른다 — 그저 더 성기게 표본을 추출할 뿐이다. 더 짧은 히스토리 충실도는 캘린더의 부분 구간을 밀도 있게 표본으로 삼는다. 그리고 시장 히스토리의 부분 구간은 특정한 하나의 레짐이다. 지난 한 달치로 설정들의 순위를 매길 때, 당신은 전체 기간 순위에 대한 시끄럽지만 편향되지 않은 추정치를 얻는 것이 아니다. 지난달의 레짐에 맞춰진 설정들을 체계적으로 편애하는 편향된 추정치를 얻는 것이다. 표본 크기를 아무리 늘려도 — 더 많은 설정, 더 많은 시행에 걸쳐 평균을 내도 — 그 편향은 줄어들지 않는다. 그것은 분산이 아니기 때문이다. 당신은 스크리닝 구간에 가장 잘 들어맞는 설정들을 승격시킬 것이고, 그것들은 바로 일시적 레짐에 과적합된 것들일 가능성이 가장 높은 설정들이다.
이것이 우리의 충실도가 폴드를 시간 순서가 아니라 인터리브 순서로 훑는 이유다. 전체 히스토리에 K개의 폴드가 흩어져 있을 때, "폴드 1개"는 중간 근처의 단일 구간이고, "폴드 2개"는 이른 구간 하나와 늦은 구간 하나이며, "폴드 3개"는 이른/중간/늦은 구간에 걸쳐 있다 — 가장 저렴한 수준을 포함해 모든 충실도 수준이 전체 캘린더에 걸쳐 표본을 추출한다. 저렴한 프록시는 전체 기간에 대한 성긴 시야이지, 그중 한 구간에 대한 날카로운 시야가 결코 아니다. 이 인터리빙 덕분에 폴드 개수 충실도는 (게이트로 고칠 수 있는) 단순한 노이즈에 그치고, (어떤 게이트로도 고칠 수 없는) 편향으로는 흐르지 않는다 — 편향된 목표에 대해 높은 ρ가 나온다는 것은 그저 당신이 레짐에 들어맞는 설정을 안정적으로 골라낼 것임을 확인해줄 뿐이다. 충실도를 저렴하게 만드는 유일한 방법이 더 최근으로 만드는 것뿐이라면, 그것은 타당한 충실도가 아니다. 대신 더 성기게 만들어라.
정직한 저렴한 축으로서의 해상도
또 다른 저렴한 축은 편향을 완전히 피하며, 우리가 체결 시뮬레이션을 위해 설명했던 적응형 드릴다운의 자연스러운 짝이다. 바로 캔들 해상도다. 전체 공간을 1시간봉으로 걸러내고, 생존자들을 1분봉으로 승격시키고, 소수의 최종 후보들에 대해서만 1초 단위 또는 원시 체결 데이터까지 드릴다운한다. 같은 전체 히스토리에 대해 더 성긴 캔들은 평가하기에 더 저렴하다 — 캔들 수가 적고, 지표 계산 패스가 더 빠르고, 시뮬레이션이 더 빠르다 — 그리고 더 짧은 구간과 달리, 전체 캘린더에 대한 성긴 시야는 편향되지 않는다. 모든 레짐을 보되, 단지 봉 내부 세부사항이 적을 뿐이다.
해상도와 폴드는 상호 보완적인 충실도 축이며, 충실도 게이트는 둘 다에 적용된다. 1시간봉 스크리닝을 신뢰하기 전에, 같은 탐침을 실행하라. 약 200개의 설정을 가져와 1시간봉과 1분봉에서 점수를 매기고, 순위 상관관계를 측정하라. ρ(1시간, 1분)가 높다면 시간봉으로 스크리닝하는 것은 안전하며, 당신은 큰 속도 향상을 정직하게 얻은 것이다. 만약 낮다면 — 이는 전략의 우위가 시간봉이 뭉개버리는 봉 내부 구조에 있을 때 발생하는데 — 시간봉 스크리닝은 그 전략에 대해 무작위 필터일 뿐이며, 게이트는 당신에게 더 정밀하게 시작하라고 알려준다. 규칙은 결코 바뀌지 않는다. 어떤 충실도에서 가지치기를 하려면, 그 순위가 진실과 일치한다는 것을 측정한 뒤에만 허용된다.
이 두 축은 서로 반대 방식으로 실패하는데, 이는 유용한 특성이다. 성긴 해상도는 봉 내부 정보를 잃고, 더 적은 폴드는 레짐 간 정보를 잃는다. 일 단위 시계에서 움직이는 모멘텀 전략은 시간봉에서는 완벽하게 순위가 매겨지지만 레짐 운을 평균화하려면 많은 폴드가 필요할 수 있다. 반면 스캘핑 전략은 폴드 수가 적어도 순위가 잘 매겨지지만 1초 해상도 이상에서는 무너질 수 있다. 축별로 실행되는 게이트는, 우연히 편리해 보이는 답을 가정하는 대신 바로 이 전략에 대해 어느 충실도에서 저렴함을 감당할 수 있는지를 알려준다.
절감 효과가 실제로 어디서 나오는가
드릴다운을 정직하게 유지해주는 단서 조항이 하나 있다. 충실도는 비용이 실제로 존재하는 곳에서만 연산을 절약해준다. 우리의 멀티 타임프레임 엔진에서 비용이 큰 부분은 지표를 미리 계산하는 것 — 멀티 타임프레임 HMA와 분리(separation) 신호 — 인데, 이는 어떤 폴드가 실행되기도 전에, 설정당 한 번 치러지는 비용이다. 캐시된 신호에 대한 폴드별 시뮬레이션은 상대적으로 저렴하다. 그래서 폴드 개수로 가지치기하는 것은 시뮬레이션 비용만 절약할 뿐, 지배적인 비용인 지표 계산 비용은 절약하지 못한다. 폴드 개수 충실도는 실재하지만, 그 상한선은 단순한 폴드 비율이 시사하는 것보다 낮다.
반면 해상도 축은 지배적인 비용을 직접 공략한다. 더 성긴 캔들은 지표를 계산할 대상 봉의 수가 더 적다는 뜻이므로, 비용이 큰 사전 계산과 저렴한 시뮬레이션이 함께 줄어든다. 이것은 사소한 디테일이 아니다 — 어떤 드릴다운을 구축할 가치가 있는지를 결정한다. 다중 충실도 탐색에 투자하기 전에, 당신의 시간이 어디에 쓰이는지 물어보라. 만약 90%가 폴드들에 걸쳐 공유되는 지표 사전 계산이라면, 폴드 개수 충실도는 얻는 것이 적고 해상도 충실도는 많은 것을 얻는다. 먼저 프로파일링하라. 올바른 저렴한 축이란 당신이 실제로 가진 비용을 제거해주는 축이며, 그것 역시 여전히 게이트를 통과해야 한다.
이 글이 연결되는 지점
충실도 게이트는 이 시리즈가 다루는 백테스트 위생 관리의 사슬에서 특정한 위치를 차지한다.
- 이는 과적합 통제의 상류에 있다. 나쁜 프록시로 가지치기하는 드릴다운은 과적합의 새로운 경로다 — 당신은 시끄러운 초기 단계가 최종 후보들을 선택하도록 내버려두고 있는 것이다. 챔피언은 여전히 Deflated Sharpe와 PBO를 통과해야 하며, 그 게이트들에 입력되는 시행 횟수는 생존자뿐 아니라 가지치기당한 모든 시행을 포함해야 한다 — 가지치기를 당했다고 해서 그 시행이 다중 검정(multiple-testing) 장부에서 면제되는 것은 아니다.
- 이는 플래토 분석(plateau analysis)과 적을 공유한다. 폴드 하나 또는 스크리닝 구간 하나에서만 이기고 다른 곳에서는 이기지 못하는 설정은, 두 도구가 모두 배격하기 위해 존재하는 바로 그 레짐 특화 산물이다. 게이트는 그런 폴드를 근거로 선택하기를 거부하고, 플래토 분석은 그런 하나에 의존해 서 있는 챔피언을 신뢰하기를 거부한다.
- 이는 그 아래에 놓인 정직한 워크포워드 분할을 전제로 한다 — 전체 캘린더에 걸친 폴드들, 따로 떼어둔 표본외 구간 — 그리고 이는 우리가 체결 시뮬레이션을 위해 구축한 드릴다운의 탐색 시점 짝이다. 같은 성긴 것에서 정밀한 것으로 가는 원리를, 얼마나 정밀하게 체결시킬지가 아니라 어떤 설정을 평가할지에 적용한 것이다.
- 그리고 이는 모든 단계에서 충실도가 누출 없이(leak-free) 유지되는 것에 절대적으로 의존한다. 만약 저렴한 단계에 비용이 큰 단계에는 없는 선견 편향(look-ahead bias)이 있다면, ρ는 오염된 목표와의 일치도를 측정하는 것이 되고 게이트는 누출을 인증해주는 꼴이 된다. 순위 상관관계를 측정하라, 그렇다 — 하지만 정직한 목적함수에 대해서 그래야 한다.
다시 말하지만, 이를 관통하는 발상은 이 시리즈가 계속해서 반복할 바로 그것이다. 백테스트는 통계적 실험이며, 그 안의 모든 지름길은 당신이 검증할 의무가 있는 하나의 가설이다. 드릴다운의 가설은 "저렴한 순위 ≈ 비싼 순위"다. 이는 약 200개의 설정으로 검증 가능하다. 검증하라.
핵심 요약
- 성긴 것에서 정밀한 것으로 가는 탐색은 단 하나의 가정에 의존한다. 저렴한 프록시가 비싼 것과 같은 방식으로 설정들의 순위를 매긴다는 것이다. 같은 값이 아니라 — 같은 순위다. 순위 상관관계가 낮으면, 공격적인 가지치기는 미래의 승자를 버리고 노이즈를 승격시킨다. 당신은 더 나쁜 답을 향해 더 빠르게 탐색하게 된다.
- 신뢰하기 전에 측정하라. 약 200개의 무작위 설정을 뽑아 모든 충실도 단계에서 점수를 매기고, 각 저렴한 단계와 전체 충실도 순위 사이의 스피어만 ρ를 계산하라. 이는 스무 줄 남짓의 코드와 저렴한 탐침 하나면 된다.
- 폴드 하나로는 거의 무작위로 순위가 매겨진다. 우리는 레짐에 따라 ρ@1이 0.03(동전 던지기)에서 0.43(여전히 신뢰 기준 미달)까지 나오는 것을 측정했다. 폴드가 누적됨에 따라 0.67, 0.78, 0.82, 0.91로 상승한다. 모든 기성 ASHA의 기본값인
min_resource = 1은, 백테스트에서는 대개 틀렸다. - 최소 충실도를 ρ ≥ 0.5가 되는 첫 번째 단계로 자동 상향하고, 어떤 단계도 통과하지 못하면 아예 가지치기를 하지 마라. 게이트는 한 설정에서는
min_resource = 2가 되었고 더 어려운 설정에서는3이 되었다. 대비책은 전체 충실도 탐색으로 우아하게 격하된다. 정확성은 어느 정도의 속도 향상을 대가로 요구한다 — 그 대가를 치러라. - 저렴한 축은 저렴한지 여부가 아니라 편향되어 있는지 여부로 선택하라. 더 짧은 히스토리는 편향되어 있다 — 레짐에 들어맞는 파라미터를 선택하며, 어떤 표본 크기도 이를 고치지 못한다. 전체 캘린더에 걸친(연속이 아니라 인터리브된) 더 적은 폴드를 사용하거나, 전체 기간에 걸친 더 성긴 해상도를 사용하라. 그리고 저렴한 축은 당신의 비용이 실제로 있는 곳에 써라.
드릴다운은 백테스팅에서 가장 훌륭한 속도 향상 기법 중 하나이면서, 동시에 스스로를 더 빨리 속이는 방법으로 가장 쉽게 변질되는 기법 중 하나이기도 하다. 이 둘의 차이는 탐색을 시작하기 전에 측정할 수 있는 단 하나의 숫자에 달려 있다. 만약 당신의 프록시가 진실과 같은 방식으로 순위를 매긴다는 것을 증명하지 못한다면, 그것은 프록시가 아니다 — 그럴듯한 비용 프로파일을 가진 난수 생성기일 뿐이다.
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.