오늘의 질문: “왜 백테스트랑 라이브가 다르게 동작하지?”
HMA 트레이딩 봇을 운영하면서 계속 신경 쓰이던 부분이 있었다. 백테스트에서는 1분봉 기준으로 트레일링 스탑이 처음부터 촘촘하게 움직이는데, 라이브 엔진에서는 일정 수익(profit_mid) 구간에 도달해야만 1분봉 보호 루프가 켜지는 구조였다.
겉보기엔 비슷해 보이지만, 실제로는 세 가지 차이가 있었다:
- 시작 시점 — 백테스트는 진입 즉시, 라이브는 수익 도달 후
- 데이터 소스 — 백테스트는 1분봉 H/L, 라이브는 ticker 현재가만
- trail anchor — 백테스트는 누적 최고점, 라이브는 현재 바의 고점
이 세 가지를 하나씩 잡아나간 이야기다.
Step 1: profit_mid 게이팅을 없애면?
먼저 use_profit_mid_trailing_1m_activation 플래그를 ON/OFF로 돌려봤다. 2025년 1월부터 최근까지의 백테스트 결과:
| 지표 | OFF (1m 즉시) | ON (profit_mid 이후) |
|---|---|---|
| Profit Factor | 1.39 | 1.35 |
| Sharpe | 3.45 | 3.25 |
| 승률 | 43.7% | 43.1% |
| MFE>1R 후 손실 | 48건 | 57건 |
1분봉을 처음부터 쓰는 게 모든 지표에서 앞섰다. 게이팅의 원래 의도는 “초반에 너무 타이트하게 잡히는 걸 방지”하는 것이었는데, 실제로는 1분봉의 세밀한 추적 덕에 수익 구간에서 놓치는 트레이드가 줄어드는 효과가 더 컸다.
Step 2: 라이브 엔진도 1분봉 OHLCV로
기존 라이브 엔진은 60초마다 fetch_ticker로 현재가 하나만 가져왔다. 그 사이에 고점이 3050을 찍었다가 내려와도, 조회 시점에 3020이면 3050은 기록되지 않는다.
해결은 간단했다. fetch_ohlcv('1m', limit=1)로 바꿔서 마지막 완성 1분봉의 H/L을 가져오는 것. API 호출 비용도 동일하고, 이렇게 하면 백테스트가 보는 데이터와 라이브가 보는 데이터가 같아진다.
Step 3: trail anchor — 현재 바 vs 누적 최고점
여기서 예상치 못한 차이를 하나 더 발견했다. 백테스트는 포지션의 역대 최고점(max_fav_move)을 anchor로 쓰는데, 라이브는 그냥 현재 바의 고점을 anchor로 쓰고 있었다.
예를 들어 고점이 3100이었다가 현재 바 고점이 3050이고, adaptive mult가 0.4로 타이트해졌을 때:
- Cumulative: trail_sl = 3100 – ATR×0.4 = 3080
- Bar: trail_sl = 3050 – ATR×0.4 = 3030
“이건 파라미터 최적화 문제 아닌가? bar에 맞춰 파라미터를 다시 튜닝하면 비슷하지 않을까?” 좋은 질문이었고, 실제로 양쪽 모두 파라미터를 최적화해서 비교했다.
| 지표 | Cumulative (최적) | Bar (최적) |
|---|---|---|
| PnL | +12.0% 기준 | -5.0% |
| Profit Factor | 1.31 | 1.27 |
| MDD | -29.3% | -40.8% |
| Sharpe | 2.19 | 1.92 |
공정하게 재최적화해도 cumulative가 확실히 우세했다. 특히 MDD 차이가 -29% vs -41%로 압도적이었다. 그래서 라이브 엔진의 30분/1분 trailing 모두 누적 최고점 anchor로 통일했다.
파라미터 최적화
정합성을 맞춘 후, 135개 조합을 스윕해서 최적 파라미터를 탐색했다. 종합 점수(PF 30% + Sharpe 25% + MDD 25% + PnL 20%) 기준 결과:
| 파라미터 | 기존값 | 최적값 |
|---|---|---|
| adaptive_trail_mult_wide | 4.5 | 5.0 |
| adaptive_trail_profit_mid | 1.0 | 0.75 |
| adaptive_trail_profit_tight | 2.0 | 2.25 |
slope, mult_mid, mult_tight도 125개 조합을 별도로 돌렸는데, 현재 값(3.0, 1.75, 0.4)이 상위 6%(8위/125개)로 이미 충분히 좋았다. 상위권은 PnL이 더 높지만 MDD가 -37~41%로 크게 악화되는 구간이라, MDD와 Sharpe를 우선하는 현재 값이 라이브 운용에 더 적합하다고 판단했다.
최종 결과
| 지표 | 변경 전 | 변경 후 | 변화 |
|---|---|---|---|
| Profit Factor | 1.25 | 1.31 | +0.06 |
| Sharpe | 1.96 | 2.19 | +0.23 |
| MDD | -33.7% | -29.3% | +4.4%p |
| Trail Capture% | 51% | 53% | +2%p |
| MFE>1R 후 손실 | 96건 | 83건 | -13건 |
정리
이번 작업의 핵심은 “성능을 높이자”가 아니라 “백테스트와 라이브가 같은 규칙으로 움직이게 하자”였다. 결과적으로 정합성을 맞추는 과정에서 성능도 따라왔다.
- 불필요한
use_profit_mid_trailing_1m_activation플래그를 제거하고 코드를 단순화했다 - 라이브 엔진이 백테스트와 동일한 데이터(1m OHLCV H/L)와 동일한 anchor(누적 최고점)를 사용하게 됐다
- 파라미터는 “가장 높은 PnL”이 아니라 “MDD와 Sharpe 밸런스”로 선택했다
백테스트 결과를 믿으려면, 먼저 라이브와 같은 조건에서 돌아가야 한다. 당연한 말인데 실제로 맞추려면 꽤 디테일한 작업이 필요했다.