Skip to content

Split View: 알고리즘 트레이딩 백테스팅 실전: Python Backtrader·전략 구현·성과 평가·리스크 관리

✨ Learn with Quiz
|

알고리즘 트레이딩 백테스팅 실전: Python Backtrader·전략 구현·성과 평가·리스크 관리

알고리즘 트레이딩 백테스팅 실전

들어가며

알고리즘 트레이딩에서 백테스팅은 전략의 생존 가능성을 검증하는 첫 번째 관문이다. 아무리 논리적으로 완벽한 전략이라도 과거 데이터에서 유의미한 성과를 보이지 못한다면 실전에서 성공할 가능성은 극히 낮다. 그러나 백테스팅 자체에도 함정이 많다. 오버피팅, Look-Ahead Bias, Survivorship Bias 등의 함정에 빠지면 백테스트에서는 훌륭한 성과를 보이지만 실전에서는 참담한 결과를 초래한다.

Python의 Backtrader는 이벤트 기반 백테스팅 프레임워크로, 전략 개발에 집중할 수 있도록 인프라를 추상화한다. 이 글에서는 Backtrader의 아키텍처를 이해하고, SMA 교차·RSI·볼린저 밴드 전략을 구현하며, Sharpe Ratio·Max Drawdown 등의 성과 지표를 계산하고, 리스크 관리 기법과 실전 배포 시 고려사항까지 다룬다.

주의: 이 글은 교육 목적으로 작성되었으며, 투자 조언이 아닙니다. 실제 트레이딩에는 상당한 리스크가 따릅니다.

백테스팅 기초 개념

백테스팅의 핵심 편향

편향 유형설명영향방지 방법
Look-Ahead Bias미래 데이터를 사용성과 과대평가시점별 데이터 접근 제한
Survivorship Bias살아남은 종목만 테스트수익률 왜곡상폐 종목 포함 데이터
Overfitting과거 데이터에 과적합실전 성과 저하Walk-Forward, 파라미터 제한
Data Snooping같은 데이터로 반복 테스트통계적 유의성 상실홀드아웃 데이터셋
Transaction Cost Bias거래 비용 미반영수익률 과대평가슬리피지·수수료 모델링

Walk-Forward 분석

import pandas as pd
import numpy as np
from datetime import datetime


class WalkForwardAnalyzer:
    """Walk-Forward 분석기: 과적합 방지를 위한 순차적 검증"""

    def __init__(self, data: pd.DataFrame, train_ratio: float = 0.7,
                 n_splits: int = 5):
        self.data = data
        self.train_ratio = train_ratio
        self.n_splits = n_splits

    def generate_splits(self) -> list[dict]:
        """학습/테스트 구간을 순차적으로 생성"""
        total_len = len(self.data)
        split_size = total_len // self.n_splits
        splits = []

        for i in range(self.n_splits):
            # 학습 구간: 시작 ~ 현재 분할점
            train_end = split_size * (i + 1)
            train_start = max(0, train_end - int(split_size * self.train_ratio * (i + 1)))

            # 테스트 구간: 학습 종료 ~ 다음 분할점
            test_start = train_end
            test_end = min(total_len, test_start + split_size)

            if test_start >= total_len:
                break

            splits.append({
                "fold": i + 1,
                "train": self.data.iloc[train_start:train_end],
                "test": self.data.iloc[test_start:test_end],
                "train_period": f"{self.data.index[train_start]} ~ {self.data.index[train_end-1]}",
                "test_period": f"{self.data.index[test_start]} ~ {self.data.index[test_end-1]}"
            })

        return splits

    def run_walk_forward(self, strategy_fn, optimize_fn) -> pd.DataFrame:
        """Walk-Forward 최적화 실행"""
        splits = self.generate_splits()
        results = []

        for split in splits:
            # 1. 학습 데이터로 파라미터 최적화
            best_params = optimize_fn(split["train"])

            # 2. 테스트 데이터로 성과 검증
            test_result = strategy_fn(split["test"], best_params)

            results.append({
                "fold": split["fold"],
                "train_period": split["train_period"],
                "test_period": split["test_period"],
                "params": best_params,
                "return": test_result["total_return"],
                "sharpe": test_result["sharpe_ratio"],
                "max_drawdown": test_result["max_drawdown"]
            })

        return pd.DataFrame(results)

Backtrader 프레임워크 아키텍처

핵심 컴포넌트

Backtrader는 Cerebro 엔진을 중심으로 Data Feed, Strategy, Broker, Analyzer가 유기적으로 작동한다.

import backtrader as bt
import yfinance as yf


# 기본 구조 이해: Cerebro 엔진 설정
cerebro = bt.Cerebro()

# 1. 데이터 피드 추가
data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", start="2020-01-01", end="2025-12-31"),
    datetime=None,  # 인덱스를 날짜로 사용
    open="Open",
    high="High",
    low="Low",
    close="Close",
    volume="Volume"
)
cerebro.adddata(data)

# 2. 전략 추가
# cerebro.addstrategy(MyStrategy)

# 3. 브로커 설정
cerebro.broker.setcash(100000)           # 초기 자본
cerebro.broker.setcommission(commission=0.001)  # 수수료 0.1%

# 4. 사이저 설정 (포지션 크기)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

# 5. 분석기 추가
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")

# 실행
print(f"초기 자산: {cerebro.broker.getvalue():,.0f}")
results = cerebro.run()
print(f"최종 자산: {cerebro.broker.getvalue():,.0f}")

데이터 피드 구성

# CSV 파일에서 데이터 로드
class CustomCSVData(bt.feeds.GenericCSVData):
    """커스텀 CSV 데이터 피드"""
    params = (
        ("dtformat", "%Y-%m-%d"),
        ("datetime", 0),
        ("open", 1),
        ("high", 2),
        ("low", 3),
        ("close", 4),
        ("volume", 5),
        ("openinterest", -1),
    )


# 여러 종목 동시 백테스트
tickers = ["AAPL", "MSFT", "GOOGL"]
cerebro = bt.Cerebro()

for ticker in tickers:
    df = yf.download(ticker, start="2020-01-01", end="2025-12-31")
    data = bt.feeds.PandasData(dataname=df, name=ticker)
    cerebro.adddata(data)

전략 구현

1. SMA 골든크로스/데드크로스 전략

class SMACrossoverStrategy(bt.Strategy):
    """이동평균 교차 전략: 골든크로스 매수, 데드크로스 매도"""

    params = (
        ("fast_period", 20),   # 단기 이동평균
        ("slow_period", 50),   # 장기 이동평균
        ("printlog", True),
    )

    def __init__(self):
        self.sma_fast = bt.indicators.SMA(
            self.data.close, period=self.params.fast_period
        )
        self.sma_slow = bt.indicators.SMA(
            self.data.close, period=self.params.slow_period
        )
        self.crossover = bt.indicators.CrossOver(self.sma_fast, self.sma_slow)

        # 주문 추적
        self.order = None
        self.buy_price = None
        self.buy_comm = None

    def log(self, txt, dt=None):
        if self.params.printlog:
            dt = dt or self.datas[0].datetime.date(0)
            print(f"[{dt}] {txt}")

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f"BUY  | Price: {order.executed.price:.2f}, "
                    f"Cost: {order.executed.value:.2f}, "
                    f"Comm: {order.executed.comm:.2f}"
                )
                self.buy_price = order.executed.price
                self.buy_comm = order.executed.comm
            else:
                self.log(
                    f"SELL | Price: {order.executed.price:.2f}, "
                    f"Cost: {order.executed.value:.2f}, "
                    f"Comm: {order.executed.comm:.2f}"
                )

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        self.order = None

    def next(self):
        if self.order:
            return  # 대기 중인 주문이 있으면 스킵

        if not self.position:
            # 포지션 없음 -> 매수 신호 확인
            if self.crossover > 0:  # 골든크로스
                self.log(f"BUY SIGNAL | Close: {self.data.close[0]:.2f}")
                self.order = self.buy()
        else:
            # 포지션 보유 -> 매도 신호 확인
            if self.crossover < 0:  # 데드크로스
                self.log(f"SELL SIGNAL | Close: {self.data.close[0]:.2f}")
                self.order = self.sell()


# 전략 실행
cerebro = bt.Cerebro()
data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", start="2020-01-01", end="2025-12-31")
)
cerebro.adddata(data)
cerebro.addstrategy(SMACrossoverStrategy, fast_period=20, slow_period=50)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.001)
results = cerebro.run()
cerebro.plot()

2. RSI 평균회귀 전략

class RSIMeanReversionStrategy(bt.Strategy):
    """RSI 기반 평균회귀 전략: 과매도 매수, 과매수 매도"""

    params = (
        ("rsi_period", 14),
        ("oversold", 30),     # 과매도 기준
        ("overbought", 70),   # 과매수 기준
        ("stake", 100),       # 거래 수량
    )

    def __init__(self):
        self.rsi = bt.indicators.RSI(
            self.data.close, period=self.params.rsi_period
        )
        self.order = None

    def next(self):
        if self.order:
            return

        if not self.position:
            # 과매도 구간 진입 -> 매수
            if self.rsi[0] < self.params.oversold:
                self.order = self.buy(size=self.params.stake)
        else:
            # 과매수 구간 진입 -> 매도
            if self.rsi[0] > self.params.overbought:
                self.order = self.sell(size=self.params.stake)

3. 볼린저 밴드 전략

class BollingerBandStrategy(bt.Strategy):
    """볼린저 밴드 전략: 하단 밴드 터치 매수, 상단 밴드 터치 매도"""

    params = (
        ("bb_period", 20),
        ("bb_dev", 2.0),      # 표준편차 배수
        ("stop_loss", 0.03),  # 3% 손절
    )

    def __init__(self):
        self.bb = bt.indicators.BollingerBands(
            self.data.close,
            period=self.params.bb_period,
            devfactor=self.params.bb_dev
        )
        self.order = None
        self.entry_price = None

    def next(self):
        if self.order:
            return

        if not self.position:
            # 가격이 하단 밴드 아래로 하락 -> 매수
            if self.data.close[0] < self.bb.lines.bot[0]:
                self.order = self.buy()
                self.entry_price = self.data.close[0]
        else:
            # 매도 조건: 상단 밴드 돌파 또는 손절
            if self.data.close[0] > self.bb.lines.top[0]:
                self.order = self.sell()
            elif self.entry_price and \
                 self.data.close[0] < self.entry_price * (1 - self.params.stop_loss):
                self.order = self.sell()  # 손절

주문 유형과 실행

Backtrader 주문 유형

주문 유형설명사용 케이스
Market현재가 즉시 체결기본 진입/청산
Limit지정가 이하/이상 체결유리한 가격 진입
Stop지정가 도달 시 Market 전환손절/브레이크아웃
StopLimit지정가 도달 시 Limit 전환정밀한 손절
StopTrail고가 추적 손절수익 보호
class AdvancedOrderStrategy(bt.Strategy):
    """다양한 주문 유형 활용 예시"""

    params = (
        ("trail_percent", 0.05),  # 5% 트레일링 스톱
        ("limit_offset", 0.02),   # 2% 리밋 오프셋
    )

    def next(self):
        if not self.position:
            # 리밋 주문: 현재가 대비 2% 낮은 가격에 매수
            limit_price = self.data.close[0] * (1 - self.params.limit_offset)
            self.buy(exectype=bt.Order.Limit, price=limit_price)

        else:
            # 트레일링 스톱: 고점 대비 5% 하락 시 매도
            self.sell(
                exectype=bt.Order.StopTrail,
                trailpercent=self.params.trail_percent
            )

성과 평가 지표

핵심 성과 지표 계산

import numpy as np
import pandas as pd


class PerformanceMetrics:
    """트레이딩 전략 성과 지표 계산"""

    def __init__(self, returns: pd.Series, risk_free_rate: float = 0.04):
        self.returns = returns
        self.risk_free_rate = risk_free_rate
        self.daily_rf = (1 + risk_free_rate) ** (1/252) - 1

    def total_return(self) -> float:
        """총 수익률"""
        return (1 + self.returns).prod() - 1

    def annualized_return(self) -> float:
        """연간화 수익률"""
        total = self.total_return()
        n_years = len(self.returns) / 252
        return (1 + total) ** (1 / n_years) - 1

    def sharpe_ratio(self) -> float:
        """샤프 비율: (연간 수익률 - 무위험 수익률) / 연간 변동성"""
        excess_returns = self.returns - self.daily_rf
        return np.sqrt(252) * excess_returns.mean() / excess_returns.std()

    def sortino_ratio(self) -> float:
        """소르티노 비율: 하방 변동성만 고려"""
        excess_returns = self.returns - self.daily_rf
        downside_returns = excess_returns[excess_returns < 0]
        downside_std = np.sqrt((downside_returns ** 2).mean())
        return np.sqrt(252) * excess_returns.mean() / downside_std

    def max_drawdown(self) -> float:
        """최대 낙폭"""
        cumulative = (1 + self.returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max
        return drawdown.min()

    def calmar_ratio(self) -> float:
        """칼마 비율: 연간 수익률 / 최대 낙폭"""
        mdd = abs(self.max_drawdown())
        if mdd == 0:
            return 0
        return self.annualized_return() / mdd

    def win_rate(self, trades: list[float]) -> float:
        """승률: 수익 거래 비율"""
        if not trades:
            return 0
        wins = sum(1 for t in trades if t > 0)
        return wins / len(trades)

    def profit_factor(self, trades: list[float]) -> float:
        """수익 팩터: 총 이익 / 총 손실"""
        gross_profit = sum(t for t in trades if t > 0)
        gross_loss = abs(sum(t for t in trades if t < 0))
        if gross_loss == 0:
            return float("inf")
        return gross_profit / gross_loss

    def summary(self) -> dict:
        """전체 성과 요약"""
        return {
            "Total Return": f"{self.total_return():.2%}",
            "Annualized Return": f"{self.annualized_return():.2%}",
            "Sharpe Ratio": f"{self.sharpe_ratio():.2f}",
            "Sortino Ratio": f"{self.sortino_ratio():.2f}",
            "Max Drawdown": f"{self.max_drawdown():.2%}",
            "Calmar Ratio": f"{self.calmar_ratio():.2f}",
        }


# 사용 예시
returns = pd.Series(np.random.normal(0.0005, 0.02, 252 * 3))  # 3년 일일 수익률
metrics = PerformanceMetrics(returns)
for key, value in metrics.summary().items():
    print(f"{key}: {value}")

Backtrader Analyzer 활용

class FullAnalysisStrategy(bt.Strategy):
    """Backtrader 내장 Analyzer를 활용한 성과 분석"""

    params = (("fast", 10), ("slow", 30))

    def __init__(self):
        self.sma_fast = bt.indicators.SMA(period=self.params.fast)
        self.sma_slow = bt.indicators.SMA(period=self.params.slow)
        self.crossover = bt.indicators.CrossOver(self.sma_fast, self.sma_slow)

    def next(self):
        if not self.position and self.crossover > 0:
            self.buy()
        elif self.position and self.crossover < 0:
            self.close()


# Analyzer 설정 및 결과 분석
cerebro = bt.Cerebro()
cerebro.addstrategy(FullAnalysisStrategy)
cerebro.adddata(data)
cerebro.broker.setcash(100000)

# 다양한 Analyzer 추가
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe",
                    timeframe=bt.TimeFrame.Days, compression=1)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn")

results = cerebro.run()
strat = results[0]

# 결과 출력
print("=== Sharpe Ratio ===")
print(f"  Sharpe: {strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')}")

print("\n=== Drawdown ===")
dd = strat.analyzers.drawdown.get_analysis()
print(f"  Max Drawdown: {dd.max.drawdown:.2f}%")
print(f"  Max Drawdown Period: {dd.max.len} days")

print("\n=== Trade Analysis ===")
ta = strat.analyzers.trades.get_analysis()
print(f"  Total Trades: {ta.total.closed}")
print(f"  Won: {ta.won.total}")
print(f"  Lost: {ta.lost.total}")

리스크 관리

포지션 사이징 전략

class KellyCriterionSizer(bt.Sizer):
    """켈리 기준 포지션 사이징"""

    params = (
        ("fraction", 0.5),  # Half-Kelly (보수적)
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        # 최근 거래 기록에서 승률과 손익비 계산
        trades = self.strategy.analyzers.trades.get_analysis()

        if hasattr(trades, "won") and trades.total.closed > 10:
            win_rate = trades.won.total / trades.total.closed
            avg_win = trades.won.pnl.average if trades.won.total > 0 else 0
            avg_loss = abs(trades.lost.pnl.average) if trades.lost.total > 0 else 1

            # Kelly: f = W - (1-W)/R, where W=승률, R=손익비
            if avg_loss > 0:
                kelly = win_rate - (1 - win_rate) / (avg_win / avg_loss)
            else:
                kelly = 0

            kelly = max(0, min(kelly * self.params.fraction, 0.25))  # 최대 25%
        else:
            kelly = 0.02  # 초기에는 2%

        target_value = cash * kelly
        size = int(target_value / data.close[0])
        return max(size, 1)


class FixedRiskSizer(bt.Sizer):
    """고정 리스크 비율 사이징: 각 거래에서 자본의 N% 이상 손실 방지"""

    params = (
        ("risk_percent", 0.02),  # 2% 리스크
        ("stop_distance", 0.05), # 5% 손절 거리
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        risk_amount = cash * self.params.risk_percent
        price = data.close[0]
        stop_distance = price * self.params.stop_distance
        size = int(risk_amount / stop_distance)
        return max(size, 1)

손절과 이익 실현

class RiskManagedStrategy(bt.Strategy):
    """손절·이익 실현·트레일링 스톱이 통합된 전략"""

    params = (
        ("sma_period", 20),
        ("stop_loss", 0.03),      # 3% 손절
        ("take_profit", 0.06),    # 6% 이익 실현 (2:1 손익비)
        ("trail_percent", 0.04),  # 4% 트레일링 스톱
    )

    def __init__(self):
        self.sma = bt.indicators.SMA(period=self.params.sma_period)
        self.order = None
        self.stop_order = None
        self.profit_order = None

    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                # 매수 체결 시 손절/이익 실현 주문 동시 설정
                stop_price = order.executed.price * (1 - self.params.stop_loss)
                profit_price = order.executed.price * (1 + self.params.take_profit)

                self.stop_order = self.sell(
                    exectype=bt.Order.Stop,
                    price=stop_price
                )
                self.profit_order = self.sell(
                    exectype=bt.Order.Limit,
                    price=profit_price
                )

            elif order.issell():
                # 매도 체결 시 반대쪽 주문 취소
                if self.stop_order and self.stop_order.status in [
                    order.Submitted, order.Accepted
                ]:
                    self.cancel(self.stop_order)
                if self.profit_order and self.profit_order.status in [
                    order.Submitted, order.Accepted
                ]:
                    self.cancel(self.profit_order)
                self.stop_order = None
                self.profit_order = None

        self.order = None

    def next(self):
        if self.order:
            return

        if not self.position:
            if self.data.close[0] > self.sma[0]:
                self.order = self.buy()

백테스팅 프레임워크 비교

Backtrader vs Zipline vs VectorBT

특성BacktraderZipline-ReloadedVectorBT
아키텍처이벤트 기반이벤트 기반벡터 연산
속도중간느림매우 빠름
학습 난이도중간높음중간
라이브 트레이딩지원미지원제한적
커뮤니티활발재활성화 중성장 중
멀티 에셋지원제한적지원
커스터마이징높음중간높음
유지보수 상태안정포크 활성활성
적합 사용자스윙 트레이더팩터 리서치퀀트 리서치
# VectorBT 기본 사용 예시 (비교 참고)
import vectorbt as vbt

# 데이터 다운로드
price = vbt.YFData.download("AAPL", start="2020-01-01", end="2025-12-31").get("Close")

# SMA 교차 백테스트 (벡터 연산으로 빠르게 실행)
fast_ma = vbt.MA.run(price, window=20)
slow_ma = vbt.MA.run(price, window=50)

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

portfolio = vbt.Portfolio.from_signals(
    price,
    entries=entries,
    exits=exits,
    init_cash=100000,
    fees=0.001
)

print(portfolio.stats())

오버피팅 방지 전략

오버피팅 경고 신호

과적합이 의심되는 상황을 판별하는 기준은 다음과 같다.

경고 신호기준대응
비현실적 수익률연간 100% 초과전략 단순화
극단적 샤프 비율3.0 초과데이터/로직 검증
파라미터 민감도소폭 변경 시 성과 급변파라미터 안정성 테스트
In-Sample/OOS 괴리학습/테스트 성과 30% 이상 차이Walk-Forward 적용
과다 파라미터5개 초과 자유 파라미터파라미터 수 축소

파라미터 안정성 테스트

class ParameterStabilityTest:
    """파라미터 섭동 테스트: 파라미터 변경에 대한 전략 안정성 검증"""

    def __init__(self, base_params: dict, perturbation: float = 0.1):
        self.base_params = base_params
        self.perturbation = perturbation

    def generate_perturbations(self) -> list[dict]:
        """기본 파라미터 주변의 섭동 조합 생성"""
        variations = []
        for key, value in self.base_params.items():
            if isinstance(value, (int, float)):
                delta = value * self.perturbation
                for factor in [-1, -0.5, 0, 0.5, 1]:
                    perturbed = self.base_params.copy()
                    new_val = value + delta * factor
                    perturbed[key] = int(new_val) if isinstance(value, int) else new_val
                    variations.append(perturbed)
        return variations

    def run_stability_test(self, backtest_fn) -> pd.DataFrame:
        """섭동된 파라미터로 백테스트를 실행하고 결과 비교"""
        variations = self.generate_perturbations()
        results = []

        for params in variations:
            result = backtest_fn(params)
            results.append({
                **params,
                "sharpe": result["sharpe_ratio"],
                "return": result["total_return"],
                "max_dd": result["max_drawdown"]
            })

        df = pd.DataFrame(results)

        # 안정성 점수: 샤프 비율의 변동 계수 (낮을수록 안정)
        stability_score = df["sharpe"].std() / abs(df["sharpe"].mean())
        print(f"Stability Score (CV): {stability_score:.4f}")
        print(f"  - 0.1 미만: 매우 안정")
        print(f"  - 0.1~0.3: 안정")
        print(f"  - 0.3 초과: 불안정 (오버피팅 의심)")

        return df

운영 시 주의사항

프로덕션 배포 체크리스트

  1. 데이터 품질: 결측치, 이상치, 분할/배당 조정 여부를 확인한다. 특히 야후 파이낸스 데이터는 수정 종가(Adj Close)를 사용해야 한다.
  2. 슬리피지 모델링: 실제 시장에서는 백테스트 가격보다 불리한 가격에 체결되는 경우가 많다. 최소 0.1% 이상의 슬리피지를 반영한다.
  3. 거래 비용: 수수료뿐 아니라 스프레드, 마켓 임팩트도 고려한다. 거래 빈도가 높을수록 영향이 크다.
  4. API 안정성: 실시간 데이터 피드와 주문 API의 연결 안정성을 확보한다. 장애 시 포지션 관리 방안을 마련한다.
  5. 모니터링: 실시간 수익률, 드로다운, 포지션 상태를 대시보드로 모니터링한다.

흔한 장애 케이스와 복구 절차

class TradingSystemRecovery:
    """트레이딩 시스템 장애 복구 핸들러"""

    def handle_data_feed_failure(self):
        """데이터 피드 장애 시"""
        # 1. 대체 데이터 소스로 전환 (예: Yahoo -> Alpha Vantage)
        # 2. 마지막 유효 가격 기반으로 포지션 유지/청산 판단
        # 3. 데이터 복구 후 누락 구간 검증
        pass

    def handle_order_rejection(self):
        """주문 거부 시"""
        # 1. 거부 사유 확인 (잔고 부족, 가격 제한 등)
        # 2. 파라미터 조정 후 재주문
        # 3. 연속 거부 시 전략 일시 중지
        pass

    def handle_position_mismatch(self):
        """포지션 불일치 시"""
        # 1. 브로커 API와 내부 상태 동기화
        # 2. 불일치 원인 분석 (부분 체결, 네트워크 오류 등)
        # 3. 수동 확인 후 포지션 조정
        pass

    def handle_extreme_drawdown(self, current_drawdown: float):
        """극단적 드로다운 발생 시"""
        # 1. 드로다운이 한계치 초과 시 (예: -15%) 신규 진입 중단
        # 2. 기존 포지션 단계적 청산
        # 3. 관리자 알림 발송
        if current_drawdown < -0.15:
            print("ALERT: Emergency stop triggered")
            # self.close_all_positions()
            # self.notify_admin()

슬리피지와 수수료 시뮬레이션

# Backtrader 슬리피지 설정
cerebro = bt.Cerebro()

# 고정 슬리피지: 각 거래에 고정 포인트 추가
cerebro.broker.set_slippage_fixed(fixed=0.05)

# 비율 슬리피지: 가격의 N% 추가
cerebro.broker.set_slippage_perc(perc=0.001)  # 0.1%

# 수수료 체계
cerebro.broker.setcommission(
    commission=0.001,    # 0.1% 수수료
    margin=None,
    mult=1.0
)

# 실전에 가까운 설정
cerebro.broker.set_slippage_perc(
    perc=0.001,          # 0.1% 슬리피지
    slip_open=True,      # 시가에도 슬리피지 적용
    slip_limit=True,     # 리밋 주문에도 적용
    slip_match=True,     # 일치하는 가격에서 적용
    slip_out=False       # 범위 밖은 적용 안 함
)

마치며

백테스팅은 알고리즘 트레이딩의 필수 과정이지만, 그 자체로 전략의 성공을 보장하지는 않는다. Backtrader는 Python 기반의 강력한 이벤트 기반 백테스팅 프레임워크로, 전략 개발부터 성과 분석까지 체계적인 워크플로우를 제공한다.

핵심은 백테스팅 결과를 맹신하지 않는 것이다. Look-Ahead Bias와 Survivorship Bias를 방지하고, Walk-Forward 분석으로 과적합을 검증하며, 파라미터 안정성 테스트로 전략의 로버스트함을 확인해야 한다. Sharpe Ratio, Maximum Drawdown, Calmar Ratio 등의 리스크 조정 성과 지표를 종합적으로 평가하고, 켈리 기준이나 고정 리스크 사이징으로 포지션을 관리해야 한다.

VectorBT는 벡터 연산으로 빠른 탐색에 적합하고, Zipline은 팩터 기반 리서치에 강점이 있다. 프로젝트의 목적과 규모에 맞는 프레임워크를 선택하되, 어떤 도구를 사용하든 리스크 관리와 오버피팅 방지라는 기본 원칙은 변하지 않는다.

참고자료

Algorithmic Trading Backtesting in Practice: Python Backtrader Strategy Implementation, Performance Evaluation, and Risk Management

Algorithmic Trading Backtesting in Practice

Introduction

In algorithmic trading, backtesting is the first gateway for verifying a strategy's viability. No matter how logically perfect a strategy appears, if it cannot demonstrate meaningful performance on historical data, its chances of success in live trading are extremely low. However, backtesting itself is fraught with pitfalls. Falling into traps like overfitting, Look-Ahead Bias, and Survivorship Bias produces strategies that look brilliant in backtests but deliver disastrous results in live trading.

Python's Backtrader is an event-driven backtesting framework that abstracts away infrastructure so you can focus on strategy development. This article covers understanding Backtrader's architecture, implementing SMA crossover, RSI, and Bollinger Bands strategies, calculating performance metrics like Sharpe Ratio and Max Drawdown, and addressing risk management techniques and production deployment considerations.

Disclaimer: This article is written for educational purposes and does not constitute investment advice. Real trading carries significant risk.

Backtesting Fundamentals

Core Backtesting Biases

Bias TypeDescriptionImpactPrevention
Look-Ahead BiasUsing future dataOverstated performanceRestrict data access by time
Survivorship BiasTesting only surviving assetsDistorted returnsInclude delisted assets
OverfittingOver-adapting to historical dataPoor live performanceWalk-Forward, parameter limits
Data SnoopingRepeated testing on same dataLost statistical significanceHold-out datasets
Transaction Cost BiasIgnoring trading costsOverstated returnsModel slippage and fees

Walk-Forward Analysis

import pandas as pd
import numpy as np
from datetime import datetime


class WalkForwardAnalyzer:
    """Walk-Forward Analyzer: sequential validation to prevent overfitting"""

    def __init__(self, data: pd.DataFrame, train_ratio: float = 0.7,
                 n_splits: int = 5):
        self.data = data
        self.train_ratio = train_ratio
        self.n_splits = n_splits

    def generate_splits(self) -> list[dict]:
        """Generate sequential train/test splits"""
        total_len = len(self.data)
        split_size = total_len // self.n_splits
        splits = []

        for i in range(self.n_splits):
            # Training period: start ~ current split point
            train_end = split_size * (i + 1)
            train_start = max(0, train_end - int(split_size * self.train_ratio * (i + 1)))

            # Testing period: training end ~ next split point
            test_start = train_end
            test_end = min(total_len, test_start + split_size)

            if test_start >= total_len:
                break

            splits.append({
                "fold": i + 1,
                "train": self.data.iloc[train_start:train_end],
                "test": self.data.iloc[test_start:test_end],
                "train_period": f"{self.data.index[train_start]} ~ {self.data.index[train_end-1]}",
                "test_period": f"{self.data.index[test_start]} ~ {self.data.index[test_end-1]}"
            })

        return splits

    def run_walk_forward(self, strategy_fn, optimize_fn) -> pd.DataFrame:
        """Execute Walk-Forward optimization"""
        splits = self.generate_splits()
        results = []

        for split in splits:
            # 1. Optimize parameters on training data
            best_params = optimize_fn(split["train"])

            # 2. Validate performance on test data
            test_result = strategy_fn(split["test"], best_params)

            results.append({
                "fold": split["fold"],
                "train_period": split["train_period"],
                "test_period": split["test_period"],
                "params": best_params,
                "return": test_result["total_return"],
                "sharpe": test_result["sharpe_ratio"],
                "max_drawdown": test_result["max_drawdown"]
            })

        return pd.DataFrame(results)

Backtrader Framework Architecture

Core Components

Backtrader operates with the Cerebro engine at its center, with Data Feed, Strategy, Broker, and Analyzer working together organically.

import backtrader as bt
import yfinance as yf


# Understanding the basic structure: Cerebro engine setup
cerebro = bt.Cerebro()

# 1. Add data feed
data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", start="2020-01-01", end="2025-12-31"),
    datetime=None,  # Use index as datetime
    open="Open",
    high="High",
    low="Low",
    close="Close",
    volume="Volume"
)
cerebro.adddata(data)

# 2. Add strategy
# cerebro.addstrategy(MyStrategy)

# 3. Broker settings
cerebro.broker.setcash(100000)           # Initial capital
cerebro.broker.setcommission(commission=0.001)  # 0.1% commission

# 4. Sizer settings (position size)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

# 5. Add analyzers
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")

# Execute
print(f"Initial portfolio value: {cerebro.broker.getvalue():,.0f}")
results = cerebro.run()
print(f"Final portfolio value: {cerebro.broker.getvalue():,.0f}")

Data Feed Configuration

# Loading data from CSV files
class CustomCSVData(bt.feeds.GenericCSVData):
    """Custom CSV data feed"""
    params = (
        ("dtformat", "%Y-%m-%d"),
        ("datetime", 0),
        ("open", 1),
        ("high", 2),
        ("low", 3),
        ("close", 4),
        ("volume", 5),
        ("openinterest", -1),
    )


# Multi-asset simultaneous backtest
tickers = ["AAPL", "MSFT", "GOOGL"]
cerebro = bt.Cerebro()

for ticker in tickers:
    df = yf.download(ticker, start="2020-01-01", end="2025-12-31")
    data = bt.feeds.PandasData(dataname=df, name=ticker)
    cerebro.adddata(data)

Strategy Implementation

1. SMA Golden Cross / Death Cross Strategy

class SMACrossoverStrategy(bt.Strategy):
    """Moving Average Crossover: buy on golden cross, sell on death cross"""

    params = (
        ("fast_period", 20),   # Short-term moving average
        ("slow_period", 50),   # Long-term moving average
        ("printlog", True),
    )

    def __init__(self):
        self.sma_fast = bt.indicators.SMA(
            self.data.close, period=self.params.fast_period
        )
        self.sma_slow = bt.indicators.SMA(
            self.data.close, period=self.params.slow_period
        )
        self.crossover = bt.indicators.CrossOver(self.sma_fast, self.sma_slow)

        # Order tracking
        self.order = None
        self.buy_price = None
        self.buy_comm = None

    def log(self, txt, dt=None):
        if self.params.printlog:
            dt = dt or self.datas[0].datetime.date(0)
            print(f"[{dt}] {txt}")

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f"BUY  | Price: {order.executed.price:.2f}, "
                    f"Cost: {order.executed.value:.2f}, "
                    f"Comm: {order.executed.comm:.2f}"
                )
                self.buy_price = order.executed.price
                self.buy_comm = order.executed.comm
            else:
                self.log(
                    f"SELL | Price: {order.executed.price:.2f}, "
                    f"Cost: {order.executed.value:.2f}, "
                    f"Comm: {order.executed.comm:.2f}"
                )

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")

        self.order = None

    def next(self):
        if self.order:
            return  # Skip if pending order exists

        if not self.position:
            # No position -> check buy signal
            if self.crossover > 0:  # Golden cross
                self.log(f"BUY SIGNAL | Close: {self.data.close[0]:.2f}")
                self.order = self.buy()
        else:
            # Holding position -> check sell signal
            if self.crossover < 0:  # Death cross
                self.log(f"SELL SIGNAL | Close: {self.data.close[0]:.2f}")
                self.order = self.sell()


# Strategy execution
cerebro = bt.Cerebro()
data = bt.feeds.PandasData(
    dataname=yf.download("AAPL", start="2020-01-01", end="2025-12-31")
)
cerebro.adddata(data)
cerebro.addstrategy(SMACrossoverStrategy, fast_period=20, slow_period=50)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.001)
results = cerebro.run()
cerebro.plot()

2. RSI Mean Reversion Strategy

class RSIMeanReversionStrategy(bt.Strategy):
    """RSI-based mean reversion: buy on oversold, sell on overbought"""

    params = (
        ("rsi_period", 14),
        ("oversold", 30),     # Oversold threshold
        ("overbought", 70),   # Overbought threshold
        ("stake", 100),       # Trade quantity
    )

    def __init__(self):
        self.rsi = bt.indicators.RSI(
            self.data.close, period=self.params.rsi_period
        )
        self.order = None

    def next(self):
        if self.order:
            return

        if not self.position:
            # Entering oversold zone -> buy
            if self.rsi[0] < self.params.oversold:
                self.order = self.buy(size=self.params.stake)
        else:
            # Entering overbought zone -> sell
            if self.rsi[0] > self.params.overbought:
                self.order = self.sell(size=self.params.stake)

3. Bollinger Bands Strategy

class BollingerBandStrategy(bt.Strategy):
    """Bollinger Bands: buy at lower band touch, sell at upper band touch"""

    params = (
        ("bb_period", 20),
        ("bb_dev", 2.0),      # Standard deviation multiplier
        ("stop_loss", 0.03),  # 3% stop-loss
    )

    def __init__(self):
        self.bb = bt.indicators.BollingerBands(
            self.data.close,
            period=self.params.bb_period,
            devfactor=self.params.bb_dev
        )
        self.order = None
        self.entry_price = None

    def next(self):
        if self.order:
            return

        if not self.position:
            # Price drops below lower band -> buy
            if self.data.close[0] < self.bb.lines.bot[0]:
                self.order = self.buy()
                self.entry_price = self.data.close[0]
        else:
            # Sell conditions: upper band breakout or stop-loss
            if self.data.close[0] > self.bb.lines.top[0]:
                self.order = self.sell()
            elif self.entry_price and \
                 self.data.close[0] < self.entry_price * (1 - self.params.stop_loss):
                self.order = self.sell()  # Stop-loss

Order Types and Execution

Backtrader Order Types

Order TypeDescriptionUse Case
MarketImmediate execution at current priceDefault entry/exit
LimitExecution at or better than specified priceFavorable entry
StopConverts to Market when price is reachedStop-loss/breakout
StopLimitConverts to Limit when price is reachedPrecise stop-loss
StopTrailTrailing stop from high priceProfit protection
class AdvancedOrderStrategy(bt.Strategy):
    """Advanced order type usage example"""

    params = (
        ("trail_percent", 0.05),  # 5% trailing stop
        ("limit_offset", 0.02),   # 2% limit offset
    )

    def next(self):
        if not self.position:
            # Limit order: buy at 2% below current price
            limit_price = self.data.close[0] * (1 - self.params.limit_offset)
            self.buy(exectype=bt.Order.Limit, price=limit_price)

        else:
            # Trailing stop: sell when 5% below peak
            self.sell(
                exectype=bt.Order.StopTrail,
                trailpercent=self.params.trail_percent
            )

Performance Evaluation Metrics

Core Performance Metric Calculations

import numpy as np
import pandas as pd


class PerformanceMetrics:
    """Trading strategy performance metric calculator"""

    def __init__(self, returns: pd.Series, risk_free_rate: float = 0.04):
        self.returns = returns
        self.risk_free_rate = risk_free_rate
        self.daily_rf = (1 + risk_free_rate) ** (1/252) - 1

    def total_return(self) -> float:
        """Total return"""
        return (1 + self.returns).prod() - 1

    def annualized_return(self) -> float:
        """Annualized return"""
        total = self.total_return()
        n_years = len(self.returns) / 252
        return (1 + total) ** (1 / n_years) - 1

    def sharpe_ratio(self) -> float:
        """Sharpe Ratio: (Annualized Return - Risk-Free Rate) / Annualized Volatility"""
        excess_returns = self.returns - self.daily_rf
        return np.sqrt(252) * excess_returns.mean() / excess_returns.std()

    def sortino_ratio(self) -> float:
        """Sortino Ratio: considers only downside volatility"""
        excess_returns = self.returns - self.daily_rf
        downside_returns = excess_returns[excess_returns < 0]
        downside_std = np.sqrt((downside_returns ** 2).mean())
        return np.sqrt(252) * excess_returns.mean() / downside_std

    def max_drawdown(self) -> float:
        """Maximum Drawdown"""
        cumulative = (1 + self.returns).cumprod()
        running_max = cumulative.expanding().max()
        drawdown = (cumulative - running_max) / running_max
        return drawdown.min()

    def calmar_ratio(self) -> float:
        """Calmar Ratio: Annualized Return / Maximum Drawdown"""
        mdd = abs(self.max_drawdown())
        if mdd == 0:
            return 0
        return self.annualized_return() / mdd

    def win_rate(self, trades: list[float]) -> float:
        """Win Rate: proportion of profitable trades"""
        if not trades:
            return 0
        wins = sum(1 for t in trades if t > 0)
        return wins / len(trades)

    def profit_factor(self, trades: list[float]) -> float:
        """Profit Factor: Gross Profit / Gross Loss"""
        gross_profit = sum(t for t in trades if t > 0)
        gross_loss = abs(sum(t for t in trades if t < 0))
        if gross_loss == 0:
            return float("inf")
        return gross_profit / gross_loss

    def summary(self) -> dict:
        """Full performance summary"""
        return {
            "Total Return": f"{self.total_return():.2%}",
            "Annualized Return": f"{self.annualized_return():.2%}",
            "Sharpe Ratio": f"{self.sharpe_ratio():.2f}",
            "Sortino Ratio": f"{self.sortino_ratio():.2f}",
            "Max Drawdown": f"{self.max_drawdown():.2%}",
            "Calmar Ratio": f"{self.calmar_ratio():.2f}",
        }


# Usage example
returns = pd.Series(np.random.normal(0.0005, 0.02, 252 * 3))  # 3 years of daily returns
metrics = PerformanceMetrics(returns)
for key, value in metrics.summary().items():
    print(f"{key}: {value}")

Using Backtrader Analyzers

class FullAnalysisStrategy(bt.Strategy):
    """Performance analysis using Backtrader built-in Analyzers"""

    params = (("fast", 10), ("slow", 30))

    def __init__(self):
        self.sma_fast = bt.indicators.SMA(period=self.params.fast)
        self.sma_slow = bt.indicators.SMA(period=self.params.slow)
        self.crossover = bt.indicators.CrossOver(self.sma_fast, self.sma_slow)

    def next(self):
        if not self.position and self.crossover > 0:
            self.buy()
        elif self.position and self.crossover < 0:
            self.close()


# Analyzer setup and result analysis
cerebro = bt.Cerebro()
cerebro.addstrategy(FullAnalysisStrategy)
cerebro.adddata(data)
cerebro.broker.setcash(100000)

# Add various Analyzers
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe",
                    timeframe=bt.TimeFrame.Days, compression=1)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn")

results = cerebro.run()
strat = results[0]

# Print results
print("=== Sharpe Ratio ===")
print(f"  Sharpe: {strat.analyzers.sharpe.get_analysis().get('sharperatio', 'N/A')}")

print("\n=== Drawdown ===")
dd = strat.analyzers.drawdown.get_analysis()
print(f"  Max Drawdown: {dd.max.drawdown:.2f}%")
print(f"  Max Drawdown Period: {dd.max.len} days")

print("\n=== Trade Analysis ===")
ta = strat.analyzers.trades.get_analysis()
print(f"  Total Trades: {ta.total.closed}")
print(f"  Won: {ta.won.total}")
print(f"  Lost: {ta.lost.total}")

Risk Management

Position Sizing Strategies

class KellyCriterionSizer(bt.Sizer):
    """Kelly Criterion position sizing"""

    params = (
        ("fraction", 0.5),  # Half-Kelly (conservative)
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        # Calculate win rate and payoff ratio from recent trade history
        trades = self.strategy.analyzers.trades.get_analysis()

        if hasattr(trades, "won") and trades.total.closed > 10:
            win_rate = trades.won.total / trades.total.closed
            avg_win = trades.won.pnl.average if trades.won.total > 0 else 0
            avg_loss = abs(trades.lost.pnl.average) if trades.lost.total > 0 else 1

            # Kelly: f = W - (1-W)/R, where W=win rate, R=payoff ratio
            if avg_loss > 0:
                kelly = win_rate - (1 - win_rate) / (avg_win / avg_loss)
            else:
                kelly = 0

            kelly = max(0, min(kelly * self.params.fraction, 0.25))  # Max 25%
        else:
            kelly = 0.02  # Initial 2%

        target_value = cash * kelly
        size = int(target_value / data.close[0])
        return max(size, 1)


class FixedRiskSizer(bt.Sizer):
    """Fixed risk percentage sizing: prevent losing more than N% of capital per trade"""

    params = (
        ("risk_percent", 0.02),  # 2% risk
        ("stop_distance", 0.05), # 5% stop distance
    )

    def _getsizing(self, comminfo, cash, data, isbuy):
        risk_amount = cash * self.params.risk_percent
        price = data.close[0]
        stop_distance = price * self.params.stop_distance
        size = int(risk_amount / stop_distance)
        return max(size, 1)

Stop-Loss and Take-Profit

class RiskManagedStrategy(bt.Strategy):
    """Strategy with integrated stop-loss, take-profit, and trailing stop"""

    params = (
        ("sma_period", 20),
        ("stop_loss", 0.03),      # 3% stop-loss
        ("take_profit", 0.06),    # 6% take-profit (2:1 risk-reward)
        ("trail_percent", 0.04),  # 4% trailing stop
    )

    def __init__(self):
        self.sma = bt.indicators.SMA(period=self.params.sma_period)
        self.order = None
        self.stop_order = None
        self.profit_order = None

    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                # On buy fill, set stop-loss and take-profit simultaneously
                stop_price = order.executed.price * (1 - self.params.stop_loss)
                profit_price = order.executed.price * (1 + self.params.take_profit)

                self.stop_order = self.sell(
                    exectype=bt.Order.Stop,
                    price=stop_price
                )
                self.profit_order = self.sell(
                    exectype=bt.Order.Limit,
                    price=profit_price
                )

            elif order.issell():
                # On sell fill, cancel the opposite order
                if self.stop_order and self.stop_order.status in [
                    order.Submitted, order.Accepted
                ]:
                    self.cancel(self.stop_order)
                if self.profit_order and self.profit_order.status in [
                    order.Submitted, order.Accepted
                ]:
                    self.cancel(self.profit_order)
                self.stop_order = None
                self.profit_order = None

        self.order = None

    def next(self):
        if self.order:
            return

        if not self.position:
            if self.data.close[0] > self.sma[0]:
                self.order = self.buy()

Backtesting Framework Comparison

Backtrader vs Zipline vs VectorBT

FeatureBacktraderZipline-ReloadedVectorBT
ArchitectureEvent-drivenEvent-drivenVectorized
SpeedMediumSlowVery fast
Learning CurveMediumHighMedium
Live TradingSupportedNot supportedLimited
CommunityActiveRevivingGrowing
Multi-AssetSupportedLimitedSupported
CustomizationHighMediumHigh
MaintenanceStableFork activeActive
Best ForSwing tradersFactor researchQuant research
# VectorBT basic usage example (for comparison)
import vectorbt as vbt

# Download data
price = vbt.YFData.download("AAPL", start="2020-01-01", end="2025-12-31").get("Close")

# SMA crossover backtest (fast execution via vectorized operations)
fast_ma = vbt.MA.run(price, window=20)
slow_ma = vbt.MA.run(price, window=50)

entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)

portfolio = vbt.Portfolio.from_signals(
    price,
    entries=entries,
    exits=exits,
    init_cash=100000,
    fees=0.001
)

print(portfolio.stats())

Overfitting Prevention Strategies

Overfitting Warning Signs

The following criteria help identify suspected overfitting.

Warning SignThresholdResponse
Unrealistic returnsOver 100% annuallySimplify strategy
Extreme Sharpe RatioOver 3.0Verify data/logic
Parameter sensitivityPerformance swings with small changesParameter stability testing
In-Sample/OOS gapOver 30% differenceApply Walk-Forward
Excessive parametersOver 5 free parametersReduce parameter count

Parameter Stability Testing

class ParameterStabilityTest:
    """Parameter perturbation test: verify strategy stability against parameter changes"""

    def __init__(self, base_params: dict, perturbation: float = 0.1):
        self.base_params = base_params
        self.perturbation = perturbation

    def generate_perturbations(self) -> list[dict]:
        """Generate perturbation combinations around base parameters"""
        variations = []
        for key, value in self.base_params.items():
            if isinstance(value, (int, float)):
                delta = value * self.perturbation
                for factor in [-1, -0.5, 0, 0.5, 1]:
                    perturbed = self.base_params.copy()
                    new_val = value + delta * factor
                    perturbed[key] = int(new_val) if isinstance(value, int) else new_val
                    variations.append(perturbed)
        return variations

    def run_stability_test(self, backtest_fn) -> pd.DataFrame:
        """Run backtests with perturbed parameters and compare results"""
        variations = self.generate_perturbations()
        results = []

        for params in variations:
            result = backtest_fn(params)
            results.append({
                **params,
                "sharpe": result["sharpe_ratio"],
                "return": result["total_return"],
                "max_dd": result["max_drawdown"]
            })

        df = pd.DataFrame(results)

        # Stability score: coefficient of variation of Sharpe ratio (lower = more stable)
        stability_score = df["sharpe"].std() / abs(df["sharpe"].mean())
        print(f"Stability Score (CV): {stability_score:.4f}")
        print(f"  - Below 0.1: Very stable")
        print(f"  - 0.1~0.3: Stable")
        print(f"  - Above 0.3: Unstable (overfitting suspected)")

        return df

Operational Notes

Production Deployment Checklist

  1. Data Quality: Verify missing values, outliers, and split/dividend adjustments. Always use Adjusted Close for Yahoo Finance data.
  2. Slippage Modeling: In real markets, fills often occur at less favorable prices than backtest prices. Reflect at least 0.1% slippage.
  3. Transaction Costs: Consider not only commissions but also spreads and market impact. Higher trading frequency amplifies the impact.
  4. API Stability: Ensure connection stability for real-time data feeds and order APIs. Have position management plans for outages.
  5. Monitoring: Monitor real-time returns, drawdown, and position status through dashboards.

Common Failure Cases and Recovery Procedures

class TradingSystemRecovery:
    """Trading system failure recovery handler"""

    def handle_data_feed_failure(self):
        """On data feed failure"""
        # 1. Switch to alternative data source (e.g., Yahoo -> Alpha Vantage)
        # 2. Decide to hold/close positions based on last valid price
        # 3. Verify missing intervals after data recovery
        pass

    def handle_order_rejection(self):
        """On order rejection"""
        # 1. Check rejection reason (insufficient funds, price limits, etc.)
        # 2. Adjust parameters and resubmit order
        # 3. Pause strategy on consecutive rejections
        pass

    def handle_position_mismatch(self):
        """On position mismatch"""
        # 1. Synchronize broker API with internal state
        # 2. Analyze mismatch cause (partial fills, network errors, etc.)
        # 3. Adjust positions after manual verification
        pass

    def handle_extreme_drawdown(self, current_drawdown: float):
        """On extreme drawdown"""
        # 1. Stop new entries when drawdown exceeds threshold (e.g., -15%)
        # 2. Gradually close existing positions
        # 3. Send admin alert notification
        if current_drawdown < -0.15:
            print("ALERT: Emergency stop triggered")
            # self.close_all_positions()
            # self.notify_admin()

Slippage and Commission Simulation

# Backtrader slippage settings
cerebro = bt.Cerebro()

# Fixed slippage: add fixed points per trade
cerebro.broker.set_slippage_fixed(fixed=0.05)

# Percentage slippage: add N% of price
cerebro.broker.set_slippage_perc(perc=0.001)  # 0.1%

# Commission structure
cerebro.broker.setcommission(
    commission=0.001,    # 0.1% commission
    margin=None,
    mult=1.0
)

# Production-realistic settings
cerebro.broker.set_slippage_perc(
    perc=0.001,          # 0.1% slippage
    slip_open=True,      # Apply slippage to open price
    slip_limit=True,     # Apply to limit orders
    slip_match=True,     # Apply at matching price
    slip_out=False       # Do not apply outside range
)

Conclusion

Backtesting is an essential process in algorithmic trading, but it does not guarantee strategy success by itself. Backtrader is a powerful Python-based event-driven backtesting framework that provides a systematic workflow from strategy development to performance analysis.

The key principle is not to blindly trust backtest results. You must prevent Look-Ahead Bias and Survivorship Bias, validate overfitting through Walk-Forward analysis, and confirm strategy robustness through parameter stability testing. Evaluate risk-adjusted performance metrics like Sharpe Ratio, Maximum Drawdown, and Calmar Ratio comprehensively, and manage positions using Kelly Criterion or fixed risk sizing.

VectorBT is well-suited for fast exploration through vectorized operations, while Zipline has strengths in factor-based research. Choose a framework appropriate for your project's purpose and scale, but regardless of the tool, the fundamental principles of risk management and overfitting prevention remain constant.

References