멀티 타임프레임 백테스트에서 선행 참조(look-ahead) 없음을 증명하기: 미래를 흔들어 과거가 그것을 볼 수 없음을 증명한다
"환상 없는 백테스트" 시리즈의 일부.
이전 글은 하나의 진단법으로 끝을 맺었다: 1봉 이동(one-bar shift) 테스트다. 모든 체결을 한 봉 늦게 옮긴다. 그 결과 성과가 무너진다면, 당신은 과거에서 거래하고 있던 것이다. 이 테스트는 선행 참조가 *체결(execution)*에 숨어 있는 단일 타임프레임 전략에는 충분하다 — 봉 i에서 결정하고 봉 i에서 체결한 경우다.
멀티 타임프레임 전략에는 더 조용히 새는 두 번째 지점이 있으며, 이동 테스트는 거기에 닿지 못한다. 1분봉에서의 결정이 1시간봉 추세를 참조할 때, 누출은 더 이상 체결이 아니라 지표에 있다 — 아직 형성이 끝나지 않은 상위 타임프레임 봉으로부터 계산된 지표 말이다. 이 글은 그 누출에 관한 것이며, 코드를 들여다보는 것이 아니라 기계적으로 — 당신의 엔진에 그것이 없음을 증명하는 방법에 관한 것이다.
마지막에 우리가 제시하는 주장은 강력하다: 우리의 멀티 TF 엔진은 실제 ETHUSDT 1분봉 데이터 86,400개 봉에 대해 25개 항목 중 25개의 정합성 및 누출 검사를 통과했다. 여기에는 미래의 모든 봉을 교란시키고 단 하나의 과거 결정도 움직이지 않음을 확인하는 미래 이동 프로브도 포함된다. 그리고 이 엔진은 라이브 봇의 정본(canonical) 코어와 비트 단위로 동일하기 때문에, 이 증명은 백테스트뿐 아니라 라이브 시스템 자체의 속성이 된다.
멀티 타임프레임이라는 지뢰밭

멀티 타임프레임(멀티 TF) 전략은 느린 시계를 참조하면서 빠른 시계로 결정을 내린다. 우리 엔진은 그 구체적인 사례다: 상위 타임프레임(HTF) 추세와 중위 타임프레임(MTF) 추세가 진입을 게이팅하고, 하위 타임프레임(LTF) 크로스가 그것을 트리거하는 트리플 TF 모멘텀 규칙이다. 1분 기준 클럭에서, 1분봉 HMA/HMA3 크로스가 1시간 추세 및 15분 추세의 방향과 일치할 때에만 진입이 발생한다. 상위 타임프레임 추세가 포지션에 반대로 반전되거나 LTF가 다시 교차할 때 청산이 발생한다.
위험은 구조적이다. 이 상위 타임프레임 값들은 각각 모든 1분봉에서 — 시간당 60번 — 답이 나와야 하며, 그 답은 라이브 봇이 그 순간에 알 수 있었던 것만을 사용해야 한다. 봉당 약 180번의 읽기(세 타임프레임, 진입 및 청산 추세, 분리 게이트) 중 단 하나라도 아직 출력되지 않은 봉을 건드리는 순간, 선행 참조가 발생한다. off-by-one 오류가 발생할 수 있는 표면적은 방대하며, 체결 누출과 달리 체결이 이동함으로써 스스로를 드러내지도 않는다.
형성 중인 봉에는 아직 최종 종가가 없다
정확한 함정은 이렇다. 지금이 10:37이고, 방금 마감된 1분봉에 대해 결정을 내리고 있다고 하자. 당신의 규칙은 1시간 추세를 필요로 한다. 10:00–11:00을 포괄하는 1시간봉 캔들은 여전히 형성 중이며 — 그 최종 종가는 10:59:59가 되어야 알 수 있다. 10:37 시점에 실제로 무엇을 알고 있는가? 그 캔들의 지금까지의 진행 중 종가, 즉 10:37 봉의 종가만 알고 있을 뿐이다. 그 캔들의 최종 종가는 23분 뒤의 미래에 있다.
순진한 멀티 TF 백테스트는 완전히 무해해 보이는 일을 한다: 1분 시리즈 전체를 처음에 한 번 1시간 단위로 리샘플한 뒤, 각 1분봉에 대해 "1시간 종가"를 읽는다. 그러나 10:00과 11:00 사이의 모든 1분봉에 대해 읽히는 값은 최종 10:00–11:00 종가다 — 실시간에서는 그 시간이 끝날 때까지 존재하지 않는 숫자다. 그 한 시간 안의 모든 결정이 조용히 최대 59분의 미래로 넘겨지는 것이다. 그리고 이것은 작은 누출이 아니다. 상위 타임프레임 종가는 지금 막 거래하려는 근미래 1분봉 수익률에 대해 거의 가장 강력한 예측 변수이므로, 그것을 누출하는 것은 정답지를 읽는 것에 가깝다. 이는 분류법에서 다룬 지표 누출 경로가 증폭된 형태다: 엿보기가 한 봉이 아니라 HTF 주기 전체에 이를 수 있다.
전편의 1봉 이동 테스트는 이것을 잡아내지 못한다. 체결을 한 봉 뒤로 옮겨도 리샘플된 1시간 시리즈는 여전히 오염되어 있다 — 누출은 언제 거래했는지가 아니라 지표가 어떻게 만들어졌는지에 있기 때문이다.
라이브 봇의 규칙을 정확히 재현하기: 마감 봉(closed-bar) 시맨틱스

올바른 규칙은 라이브 봇이 이미 실행하고 있는 규칙이다. 우리 코드베이스에서 그것은 작은 클래스인 RunningCandleBuffer이며, 라이브 틱 시뮬레이터에서 그대로 옮겨온 것이다. 이것은 기준 봉을 고정 주기 캔들로 스트리밍하며, 모든 기준 봉마다 매우 특정한 배열로부터 HTF 지표를 계산한다:
all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)
이것을 문자 그대로 읽어보라. self.closes는 이미 마감된 캔들의 최종 종가들이다 — 캔들은 새로운 주기 경계를 넘을 때만 추가되며, 저장되는 값은 그 주기 안의 마지막 기준 봉의 종가다(candle_buffer.py:39–44). 형성 중인 캔들이 기여하는 것은 정확히 하나의 숫자, self.current_close뿐이며, 이는 진행 중인 종가 — 가장 최근 기준 봉의 종가, 즉 base_close[i]다. 이것은 정의상 봉 i에서 알려진 값이다. 형성 중인 캔들의 최종 종가는 결코 사용되지 않는다. 아직 존재하지 않기 때문이다.
따라서 10:37의 HTF 지표는 [..., close(9:00 candle), close(10:37 so far)]를 본다. 10:38이 출력되면 마지막 슬롯은 close(10:38)로 갱신된다. 11:00을 넘으면 close(10:59)가 새로 마감된 캔들의 최종 값이 되고 새로운 형성 중 슬롯이 열린다. 그 한 시간 안의 어떤 시점에서도 결정이 최종 10:00–11:00 종가를 건드리는 일은 없다. 이것이 마감 봉 시맨틱스다: 마감된 캔들은 완료된 종가를 제공하고, 형성 중인 캔들은 오직 진행 중인 종가만을 제공한다.
우리의 고속 엔진(engine_multitf.py)은 이를 벡터화하고 numba로 컴파일한 재구현이다. 계속 커지는 리스트를 갖는 파이썬 루프 대신, 모든 기준 봉 i에 대해 몇 개의 캔들이 완전히 마감되었는지(n_closed[i])를 미리 계산하고, HMA/HMA3 윈도우를 [closed candle closes…, base_close[i]]로 배치한다 — 진행 중인 종가가 마지막 슬롯에 고정되는 형태다(engine_multitf.py:168–169). 이는 동일한 수학을 세 개의 타임프레임에 걸쳐 방향성 분리 게이트와 함께 속도를 위해 펼친 것일 뿐이다. 계약은 명시적이다: 봉 i에서의 값은 오직 base_close[0..i]에만 의존한다.
그것이 주장이다. 이 글의 나머지는 우리가 그것을 어떻게 증명했는가에 관한 것이다. docstring 안의 주장은 아무런 가치가 없기 때문이다.
정합성(parity)은 필요조건이지 충분조건이 아니다
스트리밍 클래스를 명시적 루프로 펼치는 벡터화된 numba 엔진은 정확히 off-by-one 오류가 번식하는 곳이다. 그래서 첫 번째 관문은 실제 데이터 구간 — ETHUSDT 1분봉, 2024년 1월–2월, 86,400개 봉 — 에 대한 정본 레퍼런스와의 비트 단위 정합성 테스트다.
우리는 두 개의 독립된 레퍼런스에 대해 두 개의 독립된 항목을 검사한다:
- 지표와 크로스 vs 봉 단위로 실행한
RunningCandleBuffer. 각 타임프레임에 대해 우리는 라이브 클래스를 전체 86,400개 봉에 걸쳐 재생하고, 크로스 이벤트 — 봉, 방향, 분리 값 — 가 정확히 일치하는지, 그리고 HMA/HMA3 값이 부동소수점 허용오차 내에서 일치하는지 비교한다(레퍼런스는np.dot을 사용하고 엔진은 명시적 루프를 사용하므로 합산 순서가 ~1e-15 수준에서 다르다). 크로스는 정확히 일치한다: HTF(1시간, HMA 길이 21)에서 408건, MTF(15분/14)에서 2,792건, LTF(1분/50)에서 3,691건의 크로스. 봉이나 방향이 다른 크로스 이벤트는 단 하나도 없다. - 트레이드 vs 레퍼런스 자체의 크로스에 의해 구동되는, 트레이딩 규칙의 독립적인 순수 파이썬 시뮬레이션. 이는 라이브 백테스트의 루프를 재현한다 — 진입 추세가 진입을 게이팅하고, 청산 추세 반전이나 반대 방향 LTF 크로스가 청산되며, 체결은
open[i+1]에서, 왕복 수수료 0.09%, 마지막 봉에서 강제 청산 — 엔진의 numba 메커니즘은 전혀 사용하지 않는다. 그런 다음 트레이드별로 비교한다: 진입/청산 봉, 방향, 진입/청산 가격, 손익(PnL), 청산 사유, 그리고 포지션 보유 총 시간.
이 테스트의 분리 임계값은 무난한 기본값이 아니다. 이는 까다로운 코너 케이스를 정확히 짚도록 선택되었다 — 일치하는 진입 임계값보다 높게 설정된 MTF 청산 임계값 — 이는 레퍼런스가 반전으로 취급하는 "포지션이 열려 있는 동안 청산 추세가 처음 정의되는" 분기를 강제한다. 정합성은 쉬운 경로뿐 아니라 코너 케이스에서도 성립해야 한다.
필드별로 트레이드는 동일하다: 듀얼 구성에서 466건의 트레이드, 트리플 구성에서 211건의 트레이드, 총 손익은 1e-12까지 일치하며 모든 트레이드의 필드가 허용오차 내에서 동일하다. 코드를 전혀 공유하지 않는 두 구현 — 컴파일된 벡터화 엔진과, 세 번째 구현의 크로스에 대한 순진한 파이썬 루프 — 이 마지막 소수점까지 동일한 트레이드를 만들어낸다.
이것은 강력한 결과이며, 선행 참조가 없다는 증명은 아니다. 정합성은 고속 엔진이 레퍼런스를 충실히 재현한다는 것을 말해줄 뿐이다. 만약 레퍼런스 자체가 누출되었다면 — 만약 RunningCandleBuffer가 엿보고 있었다면 — 정합성은 그 누출을 충실히 재현하고 통과했을 것이다. 구현 간의 일치는 그것들이 동일하다는 것을 알려줄 뿐, 인과적이라는 것을 알려주지는 않는다. 인과성을 위해서는 다른 종류의 테스트가 필요하다. 엔진에게 과거가 미래를 볼 수 있는지 직접 묻는 테스트 말이다.
미래 이동(shifted-future) 프로브: 진짜 증명

선행 참조의 정의는 조작적(operational)이므로, 조작적으로 테스트하라. 선행 참조란 과거의 결정이 미래 데이터에 의존한다는 것을 의미한다. 그 대우 명제는 실행 가능한 테스트가 된다: 미래를 바꿨는데 과거의 결정이 하나라도 움직인다면, 과거는 미래를 읽고 있었던 것이다. 그러니 미래를 바꿔라 — 잔인하게 — 그리고 과거를 지켜보라.
시리즈의 60% 지점에 절단점 j를 잡는다(86,400개 중 51,840번째 봉). j 이후의 모든 봉을 교란시킨다: 미래의 모든 종가와 시가를 1.05배 한다. 교란된 데이터로 세 타임프레임 전체의 시그널 스택을 다시 계산한다. 그런 다음 j보다 엄밀히 이전인 모든 것이 교란되지 않은 실행과 비트 단위로 동일함을 검증한다:
j = int(n * 0.6) # bar 51,840
cl2 = cl.copy(); cl2[j:] *= 1.05 # shove the future up 5%
op2 = op.copy(); op2[j:] *= 1.05
base = [precompute_tf_signals(cl, ts, p, L) for (p, L) in tf_params]
pert = [precompute_tf_signals(cl2, ts, p, L) for (p, L) in tf_params]
for s0, s1 in zip(base, pert):
assert eq_nan(s0.hma[:j], s1.hma[:j]) # HMA identical, NaNs included
assert eq_nan(s0.hma3[:j], s1.hma3[:j])
assert np.array_equal(s0.cross[:j], s1.cross[:j]) # every cross event
assert np.array_equal(s0.sep[:j], s1.sep[:j]) # every separation
"비슷하다"가 아니다. "허용오차 내"도 아니다. np.array_equal이며, NaN은 NaN과 반드시 일치해야 한다 — 51,840개의 과거 봉에서 모든 HMA 값, 모든 HMA3 값, 모든 크로스 플래그, 모든 분리 값이 동일한 부동소수점 값이어야 한다. 그런 다음 트레이드에 대해서도 동일한 검증을 수행한다: 청산이 j 이전에 있는 모든 트레이드는 필드별로 변하지 않아야 한다. 만약 미래에 대한 5%의 밀어냄이 열두 번째 소수 자리에서 단 하나의 과거 HMA라도 움직인다면, 과거의 결정이 미래를 참조한 것이며 프로브는 실패한다.
우리 엔진은 이를 통과한다 — 세 타임프레임 모두, 그리고 듀얼과 트리플 트레이드 시뮬레이션 모두에서. 34,560개의 미래 봉을 교란시켜도, 51,840개의 과거 봉과 그 안에서 청산된 모든 트레이드는 정확히 원래 그대로다. 이것은 구현 간의 일치가 아니라, 시간 상의 정보 경계가 유지된다는 직접적인 증명이다.
실패할 수 없는 테스트는 아무것도 증명하지 않는다
위의 프로브를 통과하는 방법 중, 아무것도 증명하지 않는 방법이 하나 있다: 만약 교란이 no-op이었다면 — 미래를 1.05배 곱해도 어디도 아무것도 변하지 않았다면 — "과거가 변하지 않았다"는 것은 자명하게 참이며 전혀 유익하지 않다. 실패할 수 없는 테스트에서의 초록색 체크 표시는 테스트가 없는 것보다 나쁘다. 그것이 거짓된 확신을 만들어내기 때문이다. 그래서 이 프로브에는 검증력을 부여하는 두 개의 추가 단언(assertion)이 붙어 있다.
미래는 실제로 변해야 한다. 우리는 교란이 [j, n) 어딘가에서 크로스를 실제로 변경했는지 검증한다:
assert not np.array_equal(s0.cross[j:], s1.cross[j:]) # probe has teeth
이제 결과는 의미를 갖는다: 미래를 다시 써버린 그 동일한 5% 밀어냄이 과거를 비트 단위로 완전히 동일하게 남겼다. 교란은 실제이고, 앞으로 전파되며, 절단점에서 딱 멈춘다. 한쪽으로만 향하는 누출 — 과거가 미래를 읽는 경우 — 이라면 j를 넘어 뒤로 번졌을 것이다. 그런 일은 일어나지 않는다.
경계는 정확히 현재 봉에 있어야 한다 — 하나 이르지도, 하나 늦지도 않게. 더 미묘한 실패는 인과적이지만 지연된 엔진일 수 있다: 현재 봉의 진행 중 종가를 무시하고 한 봉 늦게 반응하거나(누출은 없지만 라이브 트레이딩에는 없을 지연이 생김), 한 봉 일찍 반응하는 경우(한 봉 분량의 누출)다. 그래서 우리는 단일 봉 j를(1.02배로) 교란시키고 두 가지를 동시에 검증한다: 과거 [0, j)는 손상되지 않았고, 동시에 hma[j]는 즉시 반응한다.
cl3 = cl.copy(); cl3[j] *= 1.02 # nudge exactly one bar
s3 = precompute_tf_signals(cl3, ts, p_ltf, L_ltf)
assert eq_nan(s0.hma[:j], s3.hma[:j]) # nothing before j moves
assert s0.hma[j] != s3.hma[j] # bar j reacts on the same bar
이것으로 경계가 정확히 고정된다. 형성 중인 캔들의 진행 중 종가는 봉 j에서 지연 0, 예지 0으로 지표에 들어간다: 봉 j는 자신의 종가를 즉시 보지만, 그 이전의 어떤 봉도 이를 전혀 보지 못한다. 그것이 바로 마감 봉 시맨틱스가 서 있어야 할 칼날 같은 경계이며, 이 테스트는 엔진이 실제로 그 위에 서 있음을 확인한다.
여기 전체 관문이 있다 — 이 엔진과 조작된 백테스트 사이를 가로막는 25개 항목의 검사 전체다:
| 그룹 | 각 검사가 검증하는 내용 | 개수 |
|---|---|---|
| 지표 및 크로스 (타임프레임 3개) | RunningCandleBuffer 대비 크로스 이벤트 정확 일치; 크로스 시점의 분리 값; HMA/HMA3 값(rtol 1e-9) |
9 |
| 트레이드 (듀얼 + 트리플) | 트레이드 건수; 필드별 완전 일치; 총 손익 1e-12까지 일치; 포지션 보유 시간 | 8 |
| 미래 이동 프로브 (듀얼 + 트리플) | 과거 시그널이 비트 단위로 불변; 프로브의 검증력(미래가 실제로 변경됨); j 이전 트레이드 불변; 단일 봉 교란이 국소화됨 |
8 |
| 합계 | 25 |
처음 두 그룹은 고속 엔진이 라이브 레퍼런스와 동일하다는 것을 입증한다. 세 번째 그룹은 그 레퍼런스가 인과적이라는 것을 입증한다. 세 가지 모두가 필요하다: 누출되는 고속 엔진, 인과적이지만 누출되는 엔진, 그리고 인과적이지만 지연되는 엔진은 서로 다른 세 가지 실패 양상이며, 이 관문은 그 각각을 배제한다.
왜 이 프로브는 타임프레임과 무관한가
미래 이동 프로브의 우아함은 누출이 어디에 존재하는지 알거나 신경 쓸 필요가 없다는 데 있다. 이것은 타임프레임, 리샘플링, 캔들 경계에 대해 전혀 언급하지 않는다. 오직 하나만 묻는다: 미래를 교란시키면 과거가 움직이는가? 이것이 바로 체결 이동 테스트가 놓치는 멀티 TF 누출에 대해 정확히 옳은 도구가 되는 이유다.
순진하게 시리즈 전체를 미리 리샘플하는 버그를 직접 생각해보자. 만약 1시간 스트림이 전체 시리즈를 미리 리샘플하여 만들어졌다면, 봉 j-1000(j 이후에 마감되는 시간 안에 충분히 속해 있는)에서 읽히는 "1시간 종가"는, j 시점과 그 이후의 봉들에 의존하는 최종 종가를 가진 캔들의 최종 종가가 될 것이다. 미래를 1.05배 하면 그 최종 종가가 변한다 — 그러면 j-1000에서의 HTF 지표가 변하고, j-1000에서의 게이트가 변하며, 과거의 결정이 움직인다. 프로브는 절단점보다 천 걸음 앞선 봉에서, HTF 스트림 상에서 즉시 반응할 것이다.
우리 엔진의 HTF 스트림은 움직이지 않는다. j-1000에서 형성 중인 캔들이 기여하는 것은 오직 base_close[j-1000] — 과거의 종가 — 뿐이며, 그 캔들의 최종 종가는 경계를 지날 때까지 결코 참조되지 않기 때문이다. 이 프로브는 메커니즘에 대해서는 눈이 멀어 있지만 여전히 버그를 잡아낸다. 이것이 바로 증명에서 원하는 것이다: 행동(과거의 어떤 결정도 미래 데이터에 의존하지 않는다)을 제약하는 것이지, 구현(리샘플의 인덱스를 올바르게 잡았는가?)을 감사하는 것이 아니다. 행동은 실제로 거래되는 것이고, 구현은 그것과 일치하기를 바라는 것일 뿐이다.
백테스트와 라이브는 하나의 진실을 공유한다

이것이 전형적인 백테스트 감사보다 더 중요한 이유가 하나 더 있다. 엔진이 검증받는 대상인 레퍼런스 — RunningCandleBuffer — 는 백테스트를 좋아 보이게 만들기 위해 작성된 테스트 픽스처가 아니다. 그것은 라이브 봇 자체의 캔들 로직이며, 프로덕션에서 실행되는 틱 시뮬레이터에서 그대로 옮겨온 것이다. 프로브가 검증하는 마감 봉 규칙은, 라이브 봇이 봉마다 실행하는 바로 그 규칙이다.
따라서 정합성 관문은 이중의 역할을 한다. 고속 엔진이 레퍼런스와 일치함을 증명하고, 그 레퍼런스가 곧 라이브 코어이기 때문에 고속 엔진이 라이브와 일치함도 증명한다. 전편은 누출이 백테스트-라이브 정합성 격차에 대한 가장 깔끔한 설명이라고 경고했다 — 라이브 봇은 기계적으로 엿볼 수 없는 유일한 곳이며, 엿보는 백테스트는 실전에 들어가는 순간 괴리가 발생한다. 여기서는 그 격차가 구조적으로 닫혀 있다. 백테스트와 봇은 하나의 캔들 버퍼, 하나의 크로스 규칙, "봉 i에서 알려진 것"에 대한 하나의 정의를 공유한다. 탐색이 최적화하는 숫자가 바로 봇이 실제로 거래할 수 있는 숫자다.
이것이 선행 참조가 없다고 가정하는 대신 증명하는 것의 요점 전부다. 멀티 TF 탐색은 수천 개의 설정을 실행한다. 만약 엔진이 누출되었다면, 탐색은 그 누출을 가장 공격적으로 악용하는 설정을 찾아내어 조작된 승자를 당신에게 안겨줄 것이다 — 이것이 분류법이 순수한 노이즈로부터 샤프 지수 15를 측정했던 그 실패 양상이다. 이 프로브가 있어야 자본을 연결하기 전에 그 승자가 진짜라고 신뢰할 수 있다.
이 프로브가 증명하는 것과 증명하지 않는 것
테스트에 대한 엄밀함은 양날의 검이므로, 그 범위에 대해 정확해야 한다. 미래 이동 프로브가 증명하는 것은 단 하나의 특정한 속성이다: 이 데이터에서, 봉 j 시점이나 그 이전의 어떤 시그널 또는 트레이드 결정도 j 이후의 어떤 봉에도 의존하지 않는다는 것 — 즉 지표, 크로스, 트레이드 경로를 통틀어 시간 상의 정보 경계가 유지된다는 것이다. 이것이 바로 우리가 제거하고자 했던 멀티 TF 누출이며, 코드 리뷰로는 확립할 수 없는 속성이다.
이것은 전략에 우위(edge)가 있다는 것을 증명하지 않는다. 완벽하게 인과적인 엔진도 정직하게 돈을 잃을 수 있으며, 프로브는 그것에 대해 침묵한다 — 그래야 마땅하다. 누출이 없음을 증명하는 것과 실제 우위를 찾는 것은 별개의 작업이며, 이 둘을 혼동하는 것이 누출된 백테스트가 배포되는 원인이 된다. 이것은 시간과 무관한 편향들을 다루지 않는다: 종목의 생존 편향, 엔진이 좋아 보인 뒤에야 프로브를 실행하는 것에서 오는 선택 편향, 또는 지나치게 관대한 수수료 모델 등이다. 그리고 그 자체로는 라이브 체결이 백테스트 체결과 일치한다는 것도 증명하지 않는다 — 슬리피지와 지연은 프로브가 볼 수 없는 실제 격차다. 왜냐하면 이 프로브는 결정 경로에서 작동하는 것이지 실행 시장(execution venue)에서 작동하는 것이 아니기 때문이다. 그 격차를 닫는 것은 엔진의 캔들 코어가 라이브 봇의 것과 비트 단위로 동일하다는 별개의 사실이다.
이 프로브 자체의 설계에 대한 정직한 한 가지 유보 사항이 있다: 이것은 단일 j(시리즈의 60%)에서 절단한다. 검증하는 속성은 j에 대해 균일하다 — 봉 51,840에 특별한 것은 아무것도 없다 — 그래서 하나의 절단은 구조적 속성에 대한 공정한 테스트이지만, 더 편집증적인 버전이라면 j를 시리즈 전체에 걸쳐 스윕할 것이다. 우리는 잘 선택된 하나의 절단과 단일 봉 국소화 검사만으로 충분하다고 본다. 34,560개 봉의 미래 교란에서는 숨어 있다가 다른 절단점에서만 나타나는 누출이 있다면, 그것은 매우 이상한 버그여야 하기 때문이다. 요점은 증거의 한계를 아는 것이지, 단일 테스트가 보편 양화사인 척하는 것이 아니다.
핵심 요약
- 멀티 타임프레임 전략은 체결이 아니라 형성 중인 봉을 통해 누출된다. 완료되지 않은 HTF 주기 안의 결정은 오직 그 캔들의 진행 중 종가(가장 최근 기준 종가)만을 사용해야 하며, 결코 그 최종 종가를 사용해서는 안 된다. 전체 시리즈를 미리 리샘플하는 백테스트는 주기 내의 모든 결정을 HTF 주기 전체 분량의 미래로 넘겨준다.
- 1봉 이동 테스트는 이 누출에 닿지 못한다. 그것은 체결 선행 참조를 잡아낸다. 멀티 TF 누출은 지표가 어떻게 만들어졌는지에 있다. 다른 프로브가 필요하다.
- 라이브 규칙을 정확히 재현한 뒤, 그것을 증명하라. 우리는 봇의
RunningCandleBuffer마감 봉 시맨틱스를 벡터화 엔진으로 재구축하고 25개의 검사 뒤에 관문을 설치했다: 레퍼런스와 정확히 일치하는 크로스(408 / 2,792 / 3,691), 필드별로 동일한 트레이드(듀얼 466건, 트리플 211건), 1e-12까지의 손익. - 정합성은 필요조건이지 충분조건이 아니다. 레퍼런스와 일치한다는 것은 당신이 동일하다는 것을 증명할 뿐, 당신이 인과적이라는 것을 증명하지 않는다. 충실히 재현된 누출 레퍼런스는 여전히 누출된다.
- 미래 이동 프로브가 진짜 증명이다.
j시점 및 그 이후의 모든 봉을 교란시키고,j이전의 모든 시그널과 트레이드가 비트 단위로 변하지 않음을 검증하라. 미래가 과거를 움직일 수 있다면, 선행 참조가 존재하는 것이다. - 프로브에 검증력을 부여하라. 미래가 실제로 변했음을(교란이 no-op이 아님을) 검증하고, 단일 봉의 미세 조정이 바로 그 봉에서 반응함을(누출도 지연도 없음을) 검증하라. 실패할 수 없는 테스트는 아무것도 증명하지 않는다.
- 백테스트와 라이브가 하나의 코어를 공유할 때, 증명은 전이된다. 엔진이 라이브 봇의 캔들 로직과 비트 단위로 동일하기 때문에, 선행 참조 없음은 라이브 시스템의 속성이기도 하다 — 그리고 백테스트-라이브 정합성 격차는 구조적으로 닫힌다.
전편은 한 줄짜리 누출이 어떻게 샤프 지수 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.