OptiNod 아카데미
룩어헤드와 리페인팅 — 차트에서 완벽했던 신호가 실전엔 없었던 이유
차트에서 완벽해 보이는 신호가 실전에서는 그 시점에 존재하지 않았던 이유를, request.security와 봉 확정 메커니즘으로 풀어냅니다.
리페인팅(Repainting)은 지표가 봉이 마감된 뒤 과거 신호를 다시 그리는 현상이고, 룩어헤드 편향(Look-ahead Bias)은 백테스트가 미래 데이터를 미리 본 채 신호를 계산하는 오류입니다. 둘은 원인이 다르지만 결과가 같습니다. 지나간 차트에서는 신호가 완벽한 자리에 표시되어 있는데, 그 시점에 실시간으로 그 차트를 보고 있었다면 신호가 아예 없었거나 다른 자리에 있었습니다.
대중적 해석은 단순합니다. 지표가 신호를 주면 그 봉에서 들어가면 됩니다. 화살표가 바닥에 정확히 표시되어 있고 백테스트 승률이 70%가 넘으니 그대로 믿습니다. 이 믿음이 무너지는 지점은 실시간 매매에서입니다. 같은 지표를 라이브 차트에 띄워 두고 기다리면, 봉이 마감되기 전에 떴던 화살표가 봉이 닫히는 순간 사라지거나 한 칸 옆으로 옮겨 갑니다. 백테스트에 표시되어 있던 그 신호는 사후에 그려진 것이었습니다.
짚어야 할 핵심은 이것입니다. TradingView에서 백테스트 성적이 좋은 전략의 상당수는 미래 데이터를 끌어다 쓴 코드 때문에 좋아 보일 뿐이고, 전략 자체에는 우위가 없습니다. request.security()의 룩어헤드 설정 한 줄, 미완성 봉에서 계산하는 옵션 하나, 애초에 다시 그려지는 도구 하나가 결과를 갈라놓습니다. 메커니즘을 알면 어떤 백테스트를 걸러 내야 하는지, 라이브에서 어떤 신호를 한 봉 늦게 받아야 하는지를 판단할 수 있습니다.

신호 확정 시점과 진입 가능 시점은 다른 자리다
리페인팅과 룩어헤드를 이해하는 출발점은 봉의 두 가지 상태입니다. 마감된 과거 봉은 시가·고가·저가·종가가 고정되어 다시 바뀌지 않습니다. 진행 중인 실시간 봉은 고가·저가·종가가 마감 전까지 수없이 변합니다. TradingView 코드는 이 두 상태에서 다르게 행동하는데, 대부분의 사용자는 그 차이를 의식하지 않습니다.
지표가 "이 봉에서 매수"라는 화살표를 띄웠다고 합시다. 그 봉이 진행 중이라면 화살표는 현재까지의 종가를 기준으로 계산된 임시값입니다. 가격이 마감 전에 방향을 바꾸면 조건이 무너지고 화살표는 사라집니다. 그래서 진행 중인 봉의 신호를 보고 진입하면, 봉 마감 시점에 그 신호가 존재하지 않았던 자리에 들어가 있게 됩니다.
실전에서 쓸 수 있는 신호는 봉이 마감돼 종가가 확정된 신호뿐입니다. 신호가 확정되는 시점은 봉이 닫히는 순간이고, 그 신호를 보고 그 신호로 진입할 수 있는 시점은 빨라야 다음 봉이 열리는 순간입니다. 백테스트가 신호 봉의 종가에 체결되었다고 가정한다면 그 자체로 약간의 룩어헤드가 끼어듭니다. 봉이 닫히는 정확한 순간에 종가로 체결하는 것은 실전에서 불가능에 가깝기 때문입니다. 신호 확정과 진입 가능 사이의 이 한 칸 차이를 무시하면 백테스트가 실전보다 좋게 나옵니다.
request.security의 lookahead_on은 미래의 마감값을 끌어온다
상위 시간대(HTF) 데이터를 가져올 때 쓰는 함수가 request.security()입니다. 일봉 차트에서 주봉 종가를 참조하거나, 5분봉에서 1시간봉 추세를 끌어올 때 사용합니다. 룩어헤드 편향이 가장 자주 들어오는 곳이 이 함수의 lookahead 매개변수입니다.
request.security(syminfo.tickerid, "1W", high, lookahead=barmerge.lookahead_on)을 일봉 차트에서 호출하면, 아직 진행 중인 주봉의 최종 고가를 그 주의 첫째 날부터 미리 끌어옵니다. 공식 문서도 이 호출이 과거 봉에서 미래의 데이터를 가져오기 때문에 매우 잘못된 결과를 낼 수 있다고 적고 있습니다. 차트에서는 완벽하게 작동하는 것처럼 보이지만, 그 완벽함은 미래를 본 결과입니다.
2024년 3월 11일 주에 열린 BTC 주봉을 예로 들겠습니다. 이 주봉은 주중인 3월 14일에 73,777달러 고가를 기록하고 68,393달러에 마감했습니다. lookahead_on으로 이 주봉의 고가를 일봉에서 참조하면, 3월 11일 월요일부터 이미 73,777이라는 값이 차트에 들어옵니다. 실제로 73,777이 찍히기 사흘 전인데도 그렇습니다. 이 값으로 "주봉 고가 근처에서 숏"이라는 백테스트를 돌리면 3월 11일에 이미 그 고가를 알고 있었던 셈이라 승률이 비현실적으로 높게 나옵니다. 실전에서 3월 11일에 그 값을 알 방법은 없었습니다. 차트에서 완벽했던 그 숏 자리는 그 시점에 존재하지 않았던 정보로 만들어진 자리일 뿐입니다.

lookahead_off가 안전한 기본값이고, 미래 함수는 계산식 자체가 미래를 끌어다 쓴다
올바른 패턴은 barmerge.lookahead_off입니다. 이 설정에서는 상위 시간대 봉의 데이터를 그 봉이 실제로 마감된 뒤에야 하위 시간대로 내려보냅니다. 주봉이 일요일에 닫혀야 그 주봉의 고가·종가가 일봉에 반영되므로, 과거 봉이 미래를 보는 일이 없습니다. 설정을 생략하면 기본값이 lookahead_off이지만, 인터넷에서 복사한 코드 중에는 lookahead_on이 명시적으로 들어가 있는 경우가 많아 직접 확인해야 합니다.
확정된 상위 시간대 값을 안전하게 쓰면서도 신호를 일찍 받고 싶다면, 공식 문서가 권하는 형태가 있습니다. request.security(syminfo.tickerid, "1W", expression[1], lookahead=barmerge.lookahead_on)처럼 표현식에 [1] 오프셋을 붙여 직전에 마감된 상위 시간대 값을 참조하는 방식입니다. 이렇게 하면 과거와 실시간에서 동일하게 작동합니다. 신호가 한 봉 늦어지는 대가가 따르지만, 그 지연은 실전에서 어차피 감수해야 하는 지연입니다.
룩어헤드의 또 다른 형태는 미래 함수와 음수 오프셋입니다. close[-1]처럼 음수 인덱스로 다음 봉의 종가를 참조하면 명백히 미래 데이터를 끌어다 쓰는 것입니다. security() 안에서 오프셋 없이 lookahead_on을 쓰는 것과 같은 결과를 더 노골적으로 일으킵니다. 백테스트 결과가 비정상적으로 좋다면 코드에서 음수 인덱싱과 lookahead_on 두 가지부터 의심해 보는 것이 맞습니다.
봉 확정 전 계산하는 지표는 떴다 사라지는 신호를 만든다
지표나 전략에 calc_on_every_tick=true가 설정되어 있으면, 봉이 진행되는 동안 들어오는 틱마다 계산을 처음부터 다시 합니다. 라이브 차트에서는 이 설정 때문에 신호가 떴다 사라지기를 반복합니다. 종가가 조건을 만족하는 순간 화살표가 떴다가, 가격이 다시 움직여 종가가 바뀌면 화살표가 사라집니다.
문제는 백테스트와 실시간의 불일치입니다. 공식 문서가 지적하듯 전략은 과거 봉에서는 봉 마감 기준으로 실행되지만 calc_on_every_tick=true이면 실시간에서는 매 틱마다 실행됩니다. 두 실행 방식이 다르므로 과거 성적과 실시간 행동이 어긋납니다. 차트의 과거 구간에서는 모든 봉이 이미 마감된 상태라 신호가 깔끔하게 표시되어 있지만, 그 깔끔함은 봉이 닫힌 뒤에 본 결과일 뿐입니다.
이 문제를 막는 표준 도구가 barstate.isconfirmed입니다. 진입 조건에 and barstate.isconfirmed를 더하면 봉의 마지막 순간, 종가가 확정되는 시점에만 신호가 발생합니다. 과거 봉은 언제나 확정 상태로 취급되므로 이 조건은 과거와 실시간에서 동일하게 재현됩니다. 라이브에서 떴다 사라지는 신호를 없애려면 이 한 줄이 가장 단순한 방법입니다. 진행 중인 봉의 화살표를 보고 진입을 서두르기 전에, 그 신호가 barstate.isconfirmed 조건을 통과한 신호인지 먼저 확인해야 합니다.
본질적으로 다시 그려지는 도구는 마지막 스윙을 나중에 옮겨 그린다
설정을 바꿔도 해결되지 않는, 산식 자체가 리페인팅인 도구가 있습니다. 대표적인 것이 ZigZag입니다. ZigZag는 일정 퍼센트 이상의 반전이 확인되어야 스윙 고점·저점을 확정하는데, 마지막 스윙은 그 반전이 확인되기 전까지 가격을 따라 계속 이동합니다. 가격이 더 높이 오르면 마지막 고점이 위로 옮겨 가고, 반전이 확인된 뒤에야 그 자리에 고정됩니다.
3월 14일 BTC가 73,777달러를 기록한 사례로 돌아가 봅니다. ZigZag가 이 73,777을 스윙 고점으로 확정한 시점은 3월 14일 당일이 아닙니다. 그 뒤 가격이 충분히 빠진 3월 16일이나 19일 무렵에야 확정됩니다. 3월 19일에 BTC는 61,555달러 저점까지 밀렸습니다. ZigZag는 반전 임계값이 충족된 그 시점에 비로소 73,777에 고점을 고정하고, 그 전까지는 마지막 선이 가격을 따라 계속 움직이고 있었습니다. 지나간 차트에서는 73,777에 깔끔한 고점이 표시되어 누구나 그 자리에서 숏을 잡을 수 있었던 것처럼 보입니다. 그 고점이 확정된 시점에는 가격이 이미 12,000달러 넘게 빠진 뒤였습니다.
같은 함정이 일부 Supertrend 변형과 피보나치 자동 도구에도 있습니다. 마지막 스윙을 기준으로 되돌림 레벨을 자동으로 긋는 도구는 그 스윙이 확정되기 전까지 선이 통째로 움직입니다. 자동으로 그려진 0.618 되돌림선이 차트에서는 완벽한 지지로 작동한 것처럼 보여도, 그 선이 그 자리에 그려진 시점은 지지가 확인된 뒤일 수 있습니다. 자동 도구의 과거 모습을 진입 근거로 신뢰하면 안 됩니다.

HTF 신호를 실시간으로 그대로 따라가면 봉 안에서 뒤집힌다
리페인팅 도구의 화살표만 보고 백테스트 승률을 믿는 것이 첫 번째 실수입니다. ZigZag 기반 전략의 백테스트가 80% 승률을 보인다면, 그 승률은 스윙이 사후에 확정된 위치로 계산된 결과일 가능성이 높습니다. 실시간에서는 그 스윙이 확정되기 전에 진입 여부를 정해야 하므로, 백테스트가 가정한 진입 자리에 들어갈 수 없습니다. 승률 숫자 자체가 미래 데이터로 부풀려진 값입니다.
두 번째 오용은 상위 시간대 신호를 실시간으로 그대로 따라가는 것입니다. 4시간봉이나 일봉 신호를 5분봉 차트에서 실시간으로 보면, 상위 시간대 봉이 마감되기 전까지 그 신호는 계속 변합니다. 일봉이 양봉으로 마감될 것처럼 보여 매수 신호가 떴다가, 일봉이 닫힐 무렵 음봉으로 뒤집히면서 신호가 사라지는 일이 흔합니다. 상위 시간대 신호는 그 시간대 봉이 마감된 뒤에만 확정된 것으로 봐야 합니다. 진행 중인 일봉의 추세 색깔을 5분마다 확인하며 매매하면 같은 신호에 들어갔다 나오기를 반복하다 수수료로 계좌가 조금씩 줄어듭니다.
세 번째는 알림(Alert)입니다. TradingView 알림을 calc_on_every_tick 기반 조건에 걸어 두면 봉이 진행되는 동안 조건을 스친 모든 순간에 알림이 울립니다. 봉 마감 시점에는 조건이 풀려 있는데도 알림은 이미 발송된 뒤입니다. 알림 옵션을 "Once Per Bar Close"로 설정하지 않으면, 봉 중간에 울린 알림을 보고 진입한 자리가 봉 마감 시점에는 신호가 아니었던 자리가 됩니다.
지표가 리페인팅인지 검증하는 절차
리페인팅 여부는 코드를 읽지 않아도 차트에서 직접 확인할 수 있습니다. 의심되는 지표를 검증할 때 다음 절차를 거칩니다.
- [ ] 바 리플레이 재생: TradingView 바 리플레이(Bar Replay) 기능으로 과거 임의 시점을 잡고, 봉을 하나씩 전진시키며 신호 화살표의 위치를 기록합니다. 봉이 마감될 때 화살표가 이동하거나 사라지면 리페인팅 도구입니다.
- [ ] lookahead 설정 확인: 지표 소스 코드에서
request.security를 검색해lookahead=barmerge.lookahead_on이 오프셋([1]) 없이 쓰였는지 봅니다. 오프셋 없는lookahead_on이 하나라도 있으면 미래 누수가 들어 있습니다. - [ ] 음수 인덱스 검색: 소스에서
[-를 검색해close[-1]같은 음수 오프셋이 있는지 확인합니다. 음수 인덱싱은 다음 봉을 미리 보는 직접적인 누수입니다. - [ ] 틱 계산 옵션 확인: 전략이면
calc_on_every_tick값을 보고,true라면 진입 조건에barstate.isconfirmed가 함께 걸려 있는지 확인합니다. 이 차단이 없으면 실시간 신호가 봉 안에서 흔들립니다. - [ ] 과거·실시간 일치 검증: 같은 지표를 라이브 차트에 띄워 두고 실제 시간으로 며칠 관찰한 뒤, 그동안 발생한 신호 위치를 나중에 과거 차트에서 다시 봅니다. 두 위치가 다르면 그 지표의 백테스트는 신뢰할 수 없습니다.

리페인팅을 거르는 보조 점검
검증 절차를 다 거칠 시간이 없을 때 빠르게 걸러 내는 방법이 있습니다. 첫째, 백테스트 승률이 비정상적으로 높은 전략을 일단 의심합니다. 단순한 추세 추종 전략의 현실적인 승률은 40~55% 범위에 있습니다. 70%를 넘는 승률이 나온다면 손익비가 극단적으로 낮은 마틴게일류이거나, 미래 데이터를 끌어다 쓴 룩어헤드가 들어 있을 가능성이 높습니다. 숫자가 지나치게 좋으면 먼저 코드를 의심하는 것이 손실을 줄이는 습관입니다.
둘째, 지표 설명에 "non-repainting"이라고 적혀 있어도 그대로 믿지 않습니다. 작성자가 의도와 다르게 lookahead_on을 남겨 두거나, 멀티 타임프레임 쪽만 손보고 ZigZag 계산은 리페인팅인 채로 둔 경우가 많습니다. 설명보다 바 리플레이로 직접 확인한 결과가 우선입니다.
셋째, 신호를 항상 한 봉 늦게 받는 습관을 기본값으로 둡니다. 어떤 지표든 신호 봉의 종가를 건너뛰고 다음 봉의 시가에 진입한다고 가정하고 셋업을 짭니다. 이 한 칸의 여유가 봉 안에서 뒤집히는 신호와 확정된 신호를 갈라 주고, 백테스트와 실전 사이의 간극을 가장 크게 줄여 줍니다. 차트에서 완벽해 보이는 신호일수록, 그 완벽함이 봉이 마감된 뒤에 보이는 것인지를 먼저 물어야 합니다.