백테스트-라이브 일치성: 왜 당신의 봇은 백테스트와 다르게 거래하는가
전략을 백테스트에 통과시켰다. Sharpe 2.1, MaxDD -8%, PnL +67%. 봇을 실행했다. 한 달 후 비교했다: 같은 시그널, 같은 기간 — 하지만 라이브 PnL은 40% 낮다. 드로다운은 1.5배 더 깊다. 10번의 거래 중 2번은 전혀 실행되지 않았다.
이것은 버그가 아니다. 이것은 백테스트-라이브 괴리 — 백테스트 결과와 실제 트레이딩 간의 체계적인 불일치다. 모든 사람에게 있다. 유일한 질문은 당신이 이것을 알고 있는지, 그리고 통제할 수 있는지이다.
이 글에서는 괴리의 완전한 분류 체계, 이를 최소화하기 위한 아키텍처 패턴, 그리고 프로덕션에서 일치성을 모니터링하기 위한 실용적인 체크리스트를 제공한다.
"백테스트에서는 작동했다" 증후군

모든 알고트레이더는 이 사이클을 겪는다:
- Jupyter notebook에서 전략을 작성했다
- 과거 CSV로 백테스트를 실행했다 — 결과가 훌륭하다
- 로직을 봇으로 다시 작성했다 (종종 다른 언어나 프레임워크로)
- 실행했다 — 결과가 맞지 않는다
- 버그를 찾았지만 찾지 못했다 — "시장이 변했다"
문제는 시장이 아니다. 문제는 백테스트와 봇이 같은 현실을 다르게 모델링하는 두 개의 서로 다른 소프트웨어 제품이라는 것이다. 괴리는 불가피하지만, 체계화하고 최소화할 수 있다.
괴리의 분류 체계

모든 괴리의 원인은 네 가지 카테고리로 분류된다. 각각에 대해 — 심각도 등급(1~5)과 PnL 괴리에 대한 전형적인 기여도.
1. 데이터 괴리 (심각도: 3/5)
백테스트가 보는 데이터와 봇이 실시간으로 보는 데이터는 같지 않다.
타임스탬프. 거래소는 타임스탬프 할당에 대한 서로 다른 규칙으로 캔들을 전달한다. 한 거래소는 기간의 시작으로 캔들을 표시하고, 다른 거래소는 끝으로 표시한다. REST API는 실제 종가 후 1~3초 지연으로 캔들을 반환할 수 있다. 백테스트는 히스토리 파일의 "이상적인" 타임스탬프로 작동한다.
OHLCV 집계. 히스토리 데이터는 종종 거래소가 실시간으로 수행하는 것과는 다르게 제공업체에 의해 집계된다. 차이는 마지막 자릿수에 있지만, 임계값 시그널(MA 크로스오버, 레벨 돌파)에서는 이것이 전략의 포지션 진입 여부를 결정한다.
갭과 누락 데이터. 히스토리 데이터는 보통 깨끗하다 — 누락된 캔들은 보간으로 채워진다. 실시간에서는 WebSocket이 끊길 수 있고, 봇이 30초 동안의 데이터를 놓칠 수 있다.
PnL 괴리에 대한 전형적인 기여도: 연간 PnL의 2~5%.
2. 체결 괴리 (심각도: 5/5)

가장 위험한 괴리 클래스. 백테스트는 체결을 완벽하게 시뮬레이션하지만, 현실은 이상과 거리가 멀다.
슬리피지. 백테스트는 종가(또는 시그널 가격)에서 주문을 체결한다. 현실에서 시장가 주문은 최적 bid/ask 가격에 슬리피지를 더한 가격으로 실행되며, 슬리피지는 거래량과 유동성에 의존한다. 중간 유동성의 알트코인에서 $10K 포지션의 경우, 슬리피지는 0.05~0.3%가 될 수 있다.
번의 거래에 걸친 누적 슬리피지 공식:
여기서 는 번째 거래의 슬리피지로, 오더북 깊이에 의존한다:
레이턴시. 시그널이 생성된 순간부터 주문 체결까지 시간이 경과한다: 시그널 계산(150 ms), 요청 전송(10200 ms), 거래소에서의 매칭(1~10 ms). 백테스트에서 레이턴시 = 0. 라이브에서는 가격이 움직일 수 있다.
부분 체결. 백테스트는 주문의 100%가 즉시 체결된다고 가정한다. 현실에서 지정가 주문은 부분적으로만 체결되거나, 가격이 반전되면 전혀 체결되지 않을 수 있다. 유동성이 낮은 시장에서 시장가 주문의 경우, 주문은 오더북의 여러 레벨을 "미끄러져" 내려간다.
큐 우선순위. 최적 bid 가격에 놓인 지정가 주문은 즉시 체결되지 않는다 — 해당 레벨에 이전에 놓인 모든 주문 뒤에 대기한다. "가격이 닿았다 = 주문이 체결되었다"고 간주하는 백테스트는 체계적으로 체결률을 과대평가한다.
PnL 괴리에 대한 전형적인 기여도: 연간 PnL의 10~30%.
3. 로직 괴리 (심각도: 4/5)
백테스트와 봇 사이의 전략 코드 자체의 괴리.
분리된 코드베이스. 전형적인 안티패턴: backtests/strategy_a.py와 bot/strategy_a.py — "같은 일을 하는" 두 개의 별도 파일. 3개월의 편집 후, 이들은 필연적으로 분기한다. 누군가 백테스트에 필터를 추가하고 봇에 복제하는 것을 잊었다. 또는 반대로 — 봇에서 버그가 수정되었지만 백테스트에는 남아 있었다.
다른 프레임워크. pandas의 벡터화 연산으로 백테스트, asyncio의 이벤트 기반 로직으로 봇. 동일한 전략이더라도 엣지 케이스는 다르게 처리된다: 반올림, 조건 확인 순서, NaN 처리.
상태 관리. 백테스트는 보통 스테이트리스 — 데이터 배열을 반복한다. 봇은 스테이트풀 — 포지션, 잔고, 주문 이력을 저장한다. 봇 재시작, 상태 손실, 거래소와의 비동기화 — 이 모든 것이 괴리의 원인이다.
PnL 괴리에 대한 전형적인 기여도: 연간 PnL의 5~20%.
4. 비용 괴리 (심각도: 3/5)
거래 비용 모델링의 괴리.
펀딩 비율. 대부분의 무기한 선물 백테스트는 펀딩 비율을 전혀 고려하지 않는다. 10배 레버리지에 평균 비율 0.01%/8시간일 때, 이는 /년 — 대부분의 전략 PnL을 초과한다. 자세한 분석은 기사 펀딩 비율이 레버리지를 죽인다에 있다.
수수료. Maker/taker 수수료는 보통 모델링되지만 종종 잘못된 비율로. VIP 티어, BNB 할인, 리베이트 — 이 모든 것이 최종 결과에 영향을 미친다.
스프레드. 캔들 기반 백테스트는 bid-ask 스프레드를 보지 못한다. 1분 캔들에서 close = 3000이지만, 현실에서 bid = 2999.5, ask = 3000.5. 각 거래는 스프레드의 절반을 "비용"으로 지불한다.
PnL 괴리에 대한 전형적인 기여도: 연간 PnL의 5~15%.
누적 효과
네 카테고리 모두 동시에, 그리고 일반적으로 한 방향 — 트레이더에게 불리한 방향으로 작용한다:
백테스트 PnL 대비 20~50%의 총 괴리는 최적화되지 않은 시스템에서 정상이다. 레버리지를 사용하면 효과가 배가된다.
일치성을 위한 아키텍처 패턴
패턴 1: Shared Core (공통 코어 추출)
아이디어: 전략 코어 — 시그널 생성과 체결 로직 — 을 백테스트와 봇 모두에서 사용하는 별도의 모듈로 추출한다. 주변 인프라만 다르다: 데이터 소스와 주문 제출 메커니즘.
┌─────────────────────────────────────┐
│ strategy_core.py │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ SignalEngine │ │ OrderManager │ │
│ └──────┬──────┘ └──────┬────────┘ │
│ │ │ │
│ generate_signal() create_order()│
└─────────┬───────────────┬───────────┘
│ │
┌─────┴─────┐ ┌─────┴──────┐
│ Backtest │ │ Live │
│ DataFeed │ │ DataFeed │
│ FillModel │ │ Exchange │
└────────────┘ └────────────┘
from dataclasses import dataclass
from typing import Optional
import numpy as np
@dataclass
class Signal:
side: str # 'long' | 'short'
entry_price: float
sl_price: float
tp_price: float
size: float
timestamp: int
@dataclass
class OrderRequest:
side: str
order_type: str # 'market' | 'limit'
price: float
size: float
class StrategyCore:
"""
전략 코어. 백테스트와 라이브에서 동일한 코드.
인프라가 아닌 데이터에만 의존한다.
"""
def __init__(self, params: dict):
self.fast_period = params.get('fast_ma', 20)
self.slow_period = params.get('slow_ma', 50)
self.sl_pct = params.get('sl_pct', 0.02)
self.tp_pct = params.get('tp_pct', 0.04)
self.position: Optional[Signal] = None
self._closes: list[float] = []
def on_candle(self, timestamp: int, o: float, h: float,
l: float, c: float, v: float) -> Optional[OrderRequest]:
"""
새로운 캔들을 처리한다. OrderRequest 또는 None을 반환한다.
이 메서드는 백테스트와 봇에서 동일하게 호출된다.
"""
self._closes.append(c)
if len(self._closes) < self.slow_period:
return None
fast_ma = np.mean(self._closes[-self.fast_period:])
slow_ma = np.mean(self._closes[-self.slow_period:])
if self.position is not None:
exit_order = self._check_exit(h, l, c)
if exit_order:
self.position = None
return exit_order
if self.position is None:
if fast_ma > slow_ma and self._prev_fast_ma <= self._prev_slow_ma:
self.position = Signal(
side='long', entry_price=c,
sl_price=c * (1 - self.sl_pct),
tp_price=c * (1 + self.tp_pct),
size=1.0, timestamp=timestamp,
)
return OrderRequest('buy', 'market', c, 1.0)
self._prev_fast_ma = fast_ma
self._prev_slow_ma = slow_ma
return None
def _check_exit(self, high: float, low: float,
close: float) -> Optional[OrderRequest]:
pos = self.position
if pos.side == 'long':
if low <= pos.sl_price:
return OrderRequest('sell', 'market', pos.sl_price, pos.size)
if high >= pos.tp_price:
return OrderRequest('sell', 'market', pos.tp_price, pos.size)
return None
이제 백테스트와 봇은 동일한 StrategyCore를 사용한다:
from strategy_core import StrategyCore
def run_backtest(candles, params, fill_model):
core = StrategyCore(params)
trades = []
for candle in candles:
order = core.on_candle(
candle['timestamp'], candle['open'], candle['high'],
candle['low'], candle['close'], candle['volume'],
)
if order:
fill_price = fill_model.simulate_fill(order, candle)
trades.append({'price': fill_price, 'side': order.side})
return trades
from strategy_core import StrategyCore
async def run_live(exchange, symbol, params):
core = StrategyCore(params)
async for candle in exchange.stream_candles(symbol, '1m'):
order = core.on_candle(
candle['timestamp'], candle['open'], candle['high'],
candle['low'], candle['close'], candle['volume'],
)
if order:
await exchange.place_order(symbol, order.side,
order.order_type, order.size)
핵심 규칙: StrategyCore는 데이터가 어디서 오는지, 주문이 어디로 보내지는지 모른다. OHLCV를 받아 OrderRequest를 반환한다. 나머지는 모두 인프라 레이어의 책임이다.
패턴 2: 이벤트 기반 통합 (NautilusTrader 접근법)
NautilusTrader는 통합된 NautilusKernel — 결정론적 이벤트 기반 코어와 나노초 해상도를 가진 Rust 네이티브 엔진 — 을 통해 일치성을 구현한다. 동일한 전략 구현이 백테스트와 라이브 트레이딩 모두에서 작동한다.
아키텍처는 포트와 어댑터 패턴(헥사고날 아키텍처)에 기반한다:
┌──────────────────────────────────┐
│ NautilusKernel │
│ ┌───────────┐ ┌─────────────┐ │
│ │ Strategy │ │ RiskEngine │ │
│ │ (Python) │ │ (Rust) │ │
│ └─────┬─────┘ └──────┬──────┘ │
│ │ │ │
│ ┌─────┴───────────────┴──────┐ │
│ │ Message Bus (Rust) │ │
│ └─────┬───────────────┬──────┘ │
└────────┼───────────────┼─────────┘
│ │
┌─────┴─────┐ ┌─────┴──────┐
│ Backtest │ │ Live │
│ Adapter │ │ Adapter │
│ FillModel │ │ Exchange │
│ (L2 book) │ │ Gateway │
└────────────┘ └────────────┘
장점:
- 결정론적 리플레이. 이벤트는 엄격하게 정의된 순서로 처리된다 — 백테스트 결과는 비트 단위로 재현 가능.
- 커스텀 FillModel. 모든 체결에 대한 L2 오더북 시뮬레이션 — 슬리피지가 실제 오더북 깊이를 기반으로 시뮬레이션된다.
- 성능. 초당 최대 500만 행, RAM에 맞지 않는 데이터 처리.
- Redis + PostgreSQL. Redis를 통한 캐시와 메시지 버스, PostgreSQL을 통한 영속성 — 백테스트와 라이브에서 동일한 인프라.
패턴 3: Strategy Interface (Freqtrade 접근법)
Freqtrade는 통합된 IStrategy 인터페이스를 사용한다: 동일한 전략 클래스가 백테스트와 라이브 모두에서 작동한다. 유일한 차이는 영속화 레이어이다.
class IStrategy:
"""통합 인터페이스 — 구현은 백테스트인지 라이브인지 알지 못한다."""
def populate_indicators(self, dataframe, metadata):
"""지표를 계산한다."""
dataframe['fast_ma'] = dataframe['close'].rolling(20).mean()
dataframe['slow_ma'] = dataframe['close'].rolling(50).mean()
return dataframe
def populate_entry_trend(self, dataframe, metadata):
"""진입 시그널을 결정한다."""
dataframe.loc[
(dataframe['fast_ma'] > dataframe['slow_ma']) &
(dataframe['fast_ma'].shift(1) <= dataframe['slow_ma'].shift(1)),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe, metadata):
"""청산 시그널을 결정한다."""
dataframe.loc[
(dataframe['fast_ma'] < dataframe['slow_ma']),
'exit_long'
] = 1
return dataframe
Freqtrade는 추가로 제공한다:
- Optuna를 통한 Hyperopt — 전략 파라미터 최적화
--timeframe-detail— 체결 정밀도 향상을 위한 더 세밀한 타임프레임으로의 드릴다운 (적응형 드릴다운과 유사)
패턴 비교
| Shared Core | 이벤트 기반 (NautilusTrader) | Strategy Interface (Freqtrade) | |
|---|---|---|---|
| 구현 복잡도 | 낮음 | 높음 | 중간 |
| 일치성 수준 | 중간 | 최대 | 높음 |
| 체결 시뮬레이션 | 별도 FillModel | L2 오더북 | --timeframe-detail |
| 코어 언어 | Python | Rust + Python | Python |
| 적합 대상 | 커스텀 엔진 | 기관 트레이딩 | 빠른 시작 |
체결 시뮬레이션 정확도

체결 시뮬레이션은 체결 괴리의 주요 원인이다. 정확도의 세 가지 수준:
수준 1: 나이브 (종가에서 체결)
fill_price = candle['close']
오류: 슬리피지, 스프레드, 부분 체결을 고려하지 않는다. PnL을 체계적으로 과대평가한다.
수준 2: 슬리피지 모델
def simulate_fill(order, candle, slippage_bps=5):
"""슬리피지 포함 체결."""
base_price = candle['close']
slip = base_price * slippage_bps / 10000
if order.side == 'buy':
return base_price + slip # Buy at a higher price
else:
return base_price - slip # Sell at a lower price
오류: 고정 슬리피지는 유동성과 주문 크기를 고려하지 않는다. 나이브보다 낫지만 여전히 조잡한 모델이다.
수준 3: 1s/100ms 데이터를 사용한 적응형 드릴다운
최적 옵션: SL/TP 체결 순서를 정확하게 결정하기 위해 실제 세밀한 입도의 데이터를 사용한다. 기사 적응형 드릴다운: 가변 입도의 백테스팅에 자세히 기술되어 있다.
class RealisticFillModel:
"""
복합 체결 모델: 슬리피지 + 스프레드 + 거래량 영향.
"""
def __init__(self, avg_spread_bps=3, impact_coeff=0.1):
self.avg_spread_bps = avg_spread_bps
self.impact_coeff = impact_coeff
def simulate_fill(self, order, candle, order_size_usd):
base_price = candle['close']
spread_cost = base_price * self.avg_spread_bps / 20000
candle_volume_usd = candle['volume'] * candle['close']
participation_rate = order_size_usd / max(candle_volume_usd, 1)
impact = base_price * self.impact_coeff * np.sqrt(participation_rate)
if order.side == 'buy':
return base_price + spread_cost + impact
else:
return base_price - spread_cost - impact
시장 영향 공식 (간소화된 Almgren-Chriss 모델):
여기서 는 변동성, 는 영향 계수, 는 주문 거래량, 는 해당 기간의 시장 거래량.
실용적인 일치성 체크리스트
봇을 라이브로 실행하기 전에 각 항목을 확인한다:
코드:
- 전략이 공유 코어를 사용한다 (백테스트와 라이브에서 하나의 모듈)
- 시그널 로직이 두 곳에서 중복되지 않는다
- 유닛 테스트가 동일한 입력에 대한 동일한 코어 출력을 검증한다
- 조건 확인 순서가 동일하다 (SL이 TP 전인가? TP가 SL 전인가?)
데이터:
- 타임스탬프 형식이 동일하다 (UTC, 동일 제공업체)
- OHLCV 집계가 동일한 규칙을 사용한다
- 누락된 캔들 처리가 동일하다
- Look-ahead bias가 없다 — 백테스트가 미래를 엿보지 않는다
체결:
- 슬리피지 모델이 실제 데이터로 교정되었다
- 부분 체결이 모델링되었다 (적어도 비관적으로 추정되었다)
- 지정가 주문에 큐 우선순위 모델이 있다
- 레이턴시가 고려되었다 (시그널에서 체결까지 100~500 ms 지연)
비용:
- Maker/taker 수수료가 현재 비율로 포함되었다
- 무기한 선물에서 펀딩 비율이 고려되었다
- 스프레드가 모델링되었다 (적어도 평균값)
인프라:
- 상태 영속화: 봇이 재시작 후 포지션을 복구한다
- 재연결 로직: WebSocket이 데이터 손실 없이 재연결한다
- 로깅: 모든 주문과 체결이 사후 분석을 위해 로그에 기록된다
프로덕션에서의 괴리 모니터링
일치성은 일회성 확인이 아니라 지속적인 프로세스다. 봇을 실행한 후 괴리를 실시간으로 추적해야 한다.
섀도우 모드 (페이퍼 트레이딩)
동일한 데이터로 백테스트와 병렬로 봇을 실행한다. 봇은 시그널을 생성하지만 주문을 보내지 않는다 — 로그만 기록한다. 동시에 백테스트가 같은 데이터를 처리한다. 비교한다:
class DivergenceMonitor:
"""
백테스트와 라이브 봇의 시그널을 실시간으로 비교한다.
"""
def __init__(self, tolerance_pct=0.5):
self.tolerance = tolerance_pct / 100
self.divergences = []
def compare_signal(self, backtest_signal, live_signal, timestamp):
"""백테스트와 라이브 시그널을 비교한다."""
if backtest_signal is None and live_signal is None:
return # Both silent — OK
if (backtest_signal is None) != (live_signal is None):
self.divergences.append({
'timestamp': timestamp,
'type': 'signal_mismatch',
'backtest': backtest_signal,
'live': live_signal,
'severity': 'HIGH',
})
return
price_diff = abs(
backtest_signal.entry_price - live_signal.entry_price
) / backtest_signal.entry_price
if price_diff > self.tolerance:
self.divergences.append({
'timestamp': timestamp,
'type': 'price_divergence',
'diff_pct': price_diff * 100,
'severity': 'MEDIUM',
})
def compare_fill(self, backtest_fill, live_fill, timestamp):
"""체결을 비교한다."""
if backtest_fill and live_fill:
slippage = (live_fill['price'] - backtest_fill['price']
) / backtest_fill['price']
self.divergences.append({
'timestamp': timestamp,
'type': 'fill_divergence',
'slippage_bps': slippage * 10000,
'severity': 'LOW' if abs(slippage) < 0.001 else 'MEDIUM',
})
def report(self):
"""주간 괴리 보고서."""
from collections import Counter
severity_counts = Counter(d['severity'] for d in self.divergences)
return {
'total_divergences': len(self.divergences),
'by_severity': dict(severity_counts),
'avg_slippage_bps': np.mean([
d['slippage_bps'] for d in self.divergences
if d['type'] == 'fill_divergence'
]) if any(d['type'] == 'fill_divergence'
for d in self.divergences) else 0,
}
대시보드 메트릭
| 메트릭 | 공식 | 알림 임계값 |
|---|---|---|
| 시그널 일치율 | < 95% | |
| 평균 슬리피지 | (bps) | > 10 bps |
| 체결률 | < 90% | |
| PnL 괴리 | > 20% | |
| 레이턴시 p99 | 시그널에서 체결까지의 99 퍼센타일 | > 500 ms |
슬리피지 모델 교정
2~4주 동안 데이터를 축적한 후, 실제 데이터로 백테스트 슬리피지 모델을 교정할 수 있다:
def calibrate_slippage(live_fills: list[dict]) -> dict:
"""
실제 체결을 사용하여 슬리피지 모델을 교정한다.
live_fills: [{'expected_price': ..., 'actual_price': ..., 'size_usd': ..., 'volume_usd': ...}]
"""
slippages = []
participation_rates = []
for fill in live_fills:
slip = abs(fill['actual_price'] - fill['expected_price']
) / fill['expected_price']
part = fill['size_usd'] / max(fill['volume_usd'], 1)
slippages.append(slip)
participation_rates.append(part)
slippages = np.array(slippages)
participation_rates = np.array(participation_rates)
from scipy.optimize import curve_fit
def model(x, k, base):
return k * np.sqrt(x) + base
popt, _ = curve_fit(model, participation_rates, slippages,
p0=[0.1, 0.0001])
return {
'impact_coeff': popt[0],
'base_slippage': popt[1],
'mean_slippage_bps': np.mean(slippages) * 10000,
'p95_slippage_bps': np.percentile(slippages, 95) * 10000,
}
다른 도구와의 연결
백테스트-라이브 일치성은 고립된 작업이 아니다. "환상 없는 백테스트" 시리즈의 다른 도구들과 교차한다:
- 적응형 드릴다운 — 체결 시뮬레이션 정확도를 향상시키며, 체결 일치성의 핵심 구성 요소.
- 펀딩 비율 — 백테스트가 펀딩을 모델링하지 않으면, 레버리지 3배 초과에서 일치성은 불가능하다.
- Parquet 캐시 — 사전 계산된 타임프레임과 지표로 백테스트가 봇과 동일한 데이터를 보도록 보장한다. RunningCandleBuffer 에뮬레이션 = 실시간 업데이트.
- Polars vs Pandas — pandas(백테스트)에서 Polars(라이브)로 전환할 때, 수치 결과가 일치하는지 확인해야 한다.
- 워크포워드 — 아웃오브샘플 데이터에서의 워크포워드는 전략이 어떻게 퇴화하는지 보여준다 — 이는 인샘플 백테스트보다 라이브에 더 가깝다.
권장 사항
-
공유 코어는 필수. 시그널 생성을 위한 단일 코드베이스가 일치성의 최소 요건이다. 동일한 로직의 두 파일은 한 달 이내에 괴리를 보장한다.
-
체결 모델을 교정한다. 고정 5 bps 슬리피지가 아무것도 없는 것보다 낫다. 실제 데이터로 교정된 슬리피지 모델이 훨씬 더 낫다.
-
처음 2~4주는 섀도우 모드를 사용한다. 시그널 일치율이 95% 이상에 도달할 때까지 실제 자금으로 거래하지 않는다.
-
펀딩 비율을 모델링한다. 무기한 선물에서 이것은 선택이 아니라 필수다. 펀딩은 레버리지 5배 초과에서 모든 PnL을 소진할 수 있다.
-
모든 것을 로그에 기록한다. 모든 시그널, 모든 주문, 모든 체결 — 타임스탬프와 함께. 로그 없이는 사후 분석이 불가능하다.
-
비교를 자동화한다. 주간 DivergenceMonitor 보고서가 자동으로 도착해야 한다. PnL이 마이너스가 될 때까지 기다리지 않는다.
-
기본적으로 비관적인 백테스트. 백테스트에서 기대치를 보수적으로 설정하고 라이브에서 긍정적 놀라움을 얻는 것이 그 반대보다 낫다. 슬리피지 모델은 보수적이어야 한다.
결론
백테스트-라이브 일치성은 시스템의 속성이 아니라 프로세스다. 완벽한 일치성은 존재하지 않는다: 백테스트는 정의상 현실의 모델이며, 모델은 항상 단순화한다. 하지만 "모델이 5% 다르다"와 "모델이 50% 다르다"의 차이는 아키텍처에 의해 결정된다.
성숙도의 세 가지 수준:
- 기본. 공유 코어, 고정 슬리피지, 수수료. 괴리: 10~20%.
- 고급. 이벤트 기반 아키텍처, 적응형 드릴다운, 펀딩 모델, 섀도우 모드. 괴리: 5~10%.
- 기관 수준. L2 오더북 시뮬레이션, 교정된 영향 모델, 실시간 괴리 모니터링. 괴리: 2~5%.
당신의 과제는 자신이 어느 수준에 있는지 판단하고, 자신의 포지션 크기와 레버리지에 대해 어느 정도의 괴리를 허용할 수 있는지 이해하는 것이다.
참고 링크
- NautilusTrader — High-Performance Algorithmic Trading Platform
- Freqtrade — Free, open source crypto trading bot
- Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
- Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
- Ernest Chan — Quantitative Trading: How to Build Your Own Algorithmic Trading Business
- Hexagonal Architecture (Ports and Adapters) — Alistair Cockburn
- Optuna — Hyperparameter Optimization Framework
Citation
@article{soloviov2026backtestliveparity,
author = {Soloviov, Eugen},
title = {Backtest-live parity: why your bot trades differently from the backtest},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/backtest-live-parity},
description = {Complete taxonomy of divergences between backtesting and live trading: from slippage and partial fills to codebase desynchronization. Architectural patterns for achieving parity and a production monitoring checklist.}
}
MarketMaker.cc Team
퀀트 리서치 및 전략