백테스트와 실전이 다른 이유: 파인스크립트 리페인팅, 수수료, 슬리피지 해결 방법
“백테스트 수익률은 좋은데, 자동매매를 돌리면 결과가 전혀 다르다."
트레이딩뷰 자동매매에서 가장 많이 듣는 고민입니다. 대부분은 리페인팅과 비용을 뺀 백테스트 때문입니다. 이 글에서는 원인별로 확인하는 방법과 고치는 코드를 정리합니다.
이 글에서 배우는 것
- 리페인팅이란 무엇이고 왜 생기는지
- 상위 시간봉을 쓸 때 미래 데이터를 참조하지 않는 방법
- 수수료·슬리피지·주문 수량을 현실에 맞추는 설정
- 과최적화를 피하는 방법
※ 예제 코드는 파인스크립트 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.isconfirmed3. 원인 ② 상위 시간봉 데이터가 미래를 참조함
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으로 확정 값만 쓴다 - ☐ 수수료·슬리피지·레버리지를 실제와 같게 넣었다
- ☐ 거래 횟수가 충분하고, 다른 기간·종목에서도 크게 무너지지 않는다
- ☐ 얼러트 빈도를 "봉 마감 시 한 번"으로 했다
- ☐ 테스트넷이나 소액으로 먼저 돌려 백테스트와 실제 체결을 비교했다 → 바이빗 테스트넷 사용법
