“백테스트 수익률은 좋은데, 자동매매를 돌리면 결과가 전혀 다르다."
트레이딩뷰 자동매매에서 가장 많이 듣는 고민입니다. 대부분은 리페인팅과 비용을 뺀 백테스트 때문입니다. 이 글에서는 원인별로 확인하는 방법과 고치는 코드를 정리합니다.

이 글에서 배우는 것
  • 리페인팅이란 무엇이고 왜 생기는지
  • 상위 시간봉을 쓸 때 미래 데이터를 참조하지 않는 방법
  • 수수료·슬리피지·주문 수량을 현실에 맞추는 설정
  • 과최적화를 피하는 방법

※ 예제 코드는 파인스크립트 v6 기준입니다.

1. 리페인팅이란?

리페인팅(repainting) 은 과거 봉에 표시된 신호가 실시간에서는 다르게 나오거나, 나왔다가 사라지는 현상입니다. 과거 차트에서는 신호가 “완벽한 자리"에만 찍혀 있어 백테스트가 실제보다 훨씬 좋게 보입니다.

확인 방법: 차트를 1분봉 같은 짧은 시간봉으로 두고 스크립트를 켜 둔 채 몇십 분 지켜본 뒤, 새로고침 전후의 신호 위치가 같은지 비교합니다. 달라졌다면 리페인팅입니다.

주요 원인은 세 가지입니다.

2. 원인 ① 봉이 마감되기 전에 나온 신호

실시간 봉에서는 가격이 계속 움직이므로, 봉 중간에 교차 신호가 나왔다가 봉이 마감될 때는 사라질 수 있습니다. 과거 봉은 이미 마감된 값으로 계산되므로 이 차이가 백테스트에 드러나지 않습니다.

해결 방법

  • 전략은 기본값(calc_on_every_tick = false)으로 두면 봉이 마감될 때만 계산됩니다. 특별한 이유가 없으면 켜지 마세요.
  • 지표에서 alert() 를 쓴다면 빈도를 alert.freq_once_per_bar_close 로 합니다.
  • 얼러트 설정의 빈도는 “봉 마감 시 한 번(Once Per Bar Close)” 을 선택합니다.
  • 신호 조건에 barstate.isconfirmed 를 더해 마감된 봉에서만 신호를 냅니다.
buySignal = ta.crossover(fast, slow) and barstate.isconfirmed

3. 원인 ② 상위 시간봉 데이터가 미래를 참조함

15분봉 차트에서 일봉 종가를 가져오면, 과거 봉에서는 그날의 최종 종가(= 그 시점에는 알 수 없던 미래 값)가 들어갈 수 있습니다.

// ✗ 위험: lookahead_on 을 과거 봉 참조 없이 사용 → 미래 데이터
dailyClose = request.security(syminfo.tickerid, "D", close, lookahead = barmerge.lookahead_on)

// △ 기본값(lookahead_off): 과거 봉은 괜찮지만 실시간 봉에서는 값이 계속 바뀜
dailyClose = request.security(syminfo.tickerid, "D", close)

// ✓ 권장: 확정된 전일 종가를 사용 → 과거와 실시간이 같음
dailyClose = request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on)

※ close[1] 과 lookahead_on 을 함께 써야 합니다. 둘 중 하나만 쓰면 리페인팅이 생깁니다.

상위 시간봉을 활용하는 전략 예제는 멀티 타임프레임 사용법에서 자세히 다룹니다.

4. 원인 ③ 하이킨아시 등 특수 차트에서의 백테스트

하이킨아시, 렌코 차트의 가격은 실제 거래 가격이 아닙니다. 이 차트에서 전략을 돌리면 실제로는 체결될 수 없는 가격으로 거래한 것처럼 계산됩니다.

해결 방법: 일반 캔들 차트에서 전략을 돌리고, 하이킨아시 값은 코드에서 계산하거나, strategy() 에 아래 옵션을 넣어 실제 가격으로 체결되게 합니다.

strategy("하이킨아시 전략", overlay = true, fill_orders_on_standard_ohlc = true)

5. 비용과 체결 조건을 현실에 맞추기

리페인팅이 없어도, 비용을 빼면 백테스트는 항상 실제보다 좋게 나옵니다.

 1//@version=6
 2strategy("현실적인 백테스트 설정", overlay = true,
 3     initial_capital   = 1000,                          // 실제로 넣을 금액
 4     default_qty_type  = strategy.percent_of_equity,
 5     default_qty_value = 20,                            // 한 번에 자산의 20%
 6     commission_type   = strategy.commission.percent,
 7     commission_value  = 0.05,                          // 거래소 수수료(%)
 8     slippage          = 2,                             // 슬리피지(틱)
 9     margin_long       = 100,                           // 레버리지 없음 (10배면 10)
10     margin_short      = 100)
설정확인할 점
commission_value사용하는 거래소·등급의 시장가(테이커) 수수료. 매수·매도 양쪽에 붙습니다.
slippage얼러트 후 시장가 주문이 체결될 때까지 밀리는 가격. 거래량이 적은 코인은 크게 잡습니다.
initial_capital, 주문 수량실제 운용 금액과 비율로 맞춥니다. 수량이 너무 크면 실전에서 체결이 어렵습니다.
margin_long/short선물에서 쓰는 레버리지와 같게 설정합니다.

전략의 주문은 기본적으로 신호가 난 다음 봉의 시가에 체결됩니다. 얼러트도 봉 마감 후 전송되므로 실전 자동매매와 가장 가까운 설정입니다.

6. 과최적화 피하기

설정값을 이리저리 바꿔 과거에 가장 잘 맞는 값을 찾으면, 과거에만 맞고 미래에는 맞지 않는 전략이 되기 쉽습니다.

  • 거래 횟수가 충분한지 확인합니다. (최소 수십~100회 이상)
  • 기간을 나눠 앞 기간에서 찾은 값을 뒤 기간에서 검증합니다. 트레이딩뷰의 기간 설정(전략 테스터 날짜 범위)을 바꿔 확인할 수 있습니다.
  • 최적값 하나보다 주변 값에서도 결과가 비슷한지 봅니다. 값 하나만 튀게 좋다면 우연일 가능성이 큽니다.
  • 여러 코인·종목에 같은 설정을 적용해 봅니다.

값을 손으로 하나씩 바꾸기 번거롭다면 TVExtBot 옵티마이저로 설정값 조합을 자동으로 백테스트해 순이익·승률·MDD를 비교할 수 있습니다. 결과 표에서 한 값만 튀는 조합보다, 주변 값도 고르게 좋은 구간을 고르는 것이 과최적화를 피하는 방법입니다. 옵티마이저에 맞게 input을 만드는 방법은 옵티마이저에 맞는 input 설계법을 참고해 주세요.

7. 실전 전 체크리스트

  • ☐ 1분봉에서 새로고침 전후 신호 위치가 같다 (리페인팅 없음)
  • ☐ request.security() 는 [1] + lookahead_on 으로 확정 값만 쓴다
  • ☐ 수수료·슬리피지·레버리지를 실제와 같게 넣었다
  • ☐ 거래 횟수가 충분하고, 다른 기간·종목에서도 크게 무너지지 않는다
  • ☐ 얼러트 빈도를 "봉 마감 시 한 번"으로 했다
  • ☐ 테스트넷이나 소액으로 먼저 돌려 백테스트와 실제 체결을 비교했다 → 바이빗 테스트넷 사용법

검증한 전략을 자동매매로 연결하려면 파인스크립트 전략을 사용하여 자동매매하는 방법을 참고해 주세요.