# Phase 5: Backtest Engine 설계 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 퀀트 전략 백테스트 엔진 구현 - 과거 데이터 기반 전략 성과 시뮬레이션 **핵심 기능:** - 비동기 백테스트 실행 (스레드 기반) - DB 저장으로 결과 조회/비교 가능 - 벤치마크(KOSPI) 대비 성과 비교 - 거래 비용(수수료, 슬리피지) 반영 - 리밸런싱 주기: 월별/분기별/반기별/연별 --- ## 1. 데이터 모델 ### 1.1 backtests (백테스트 작업) ```sql CREATE TABLE backtests ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), strategy_type VARCHAR(50) NOT NULL, -- multi_factor/quality/value_momentum strategy_params JSONB, -- 전략 파라미터 start_date DATE NOT NULL, end_date DATE NOT NULL, rebalance_period VARCHAR(20) NOT NULL, -- monthly/quarterly/semi_annual/annual initial_capital DECIMAL(20,2) NOT NULL, commission_rate DECIMAL(10,6) DEFAULT 0.00015, slippage_rate DECIMAL(10,6) DEFAULT 0.001, benchmark VARCHAR(20) DEFAULT 'KOSPI', status VARCHAR(20) DEFAULT 'pending', -- pending/running/completed/failed created_at TIMESTAMP DEFAULT NOW(), completed_at TIMESTAMP, error_message TEXT ); ``` ### 1.2 backtest_results (결과 지표) ```sql CREATE TABLE backtest_results ( backtest_id INTEGER PRIMARY KEY REFERENCES backtests(id), total_return DECIMAL(10,4), -- 총 수익률 (%) cagr DECIMAL(10,4), -- 연평균 수익률 (%) mdd DECIMAL(10,4), -- 최대 낙폭 (%) sharpe_ratio DECIMAL(10,4), volatility DECIMAL(10,4), -- 연간 변동성 (%) benchmark_return DECIMAL(10,4), -- 벤치마크 수익률 (%) excess_return DECIMAL(10,4) -- 초과 수익률 (%) ); ``` ### 1.3 backtest_equity_curve (자산 추이) ```sql CREATE TABLE backtest_equity_curve ( backtest_id INTEGER REFERENCES backtests(id), date DATE, portfolio_value DECIMAL(20,2), benchmark_value DECIMAL(20,2), drawdown DECIMAL(10,4), PRIMARY KEY (backtest_id, date) ); ``` ### 1.4 backtest_holdings (리밸런싱 시점 보유 종목) ```sql CREATE TABLE backtest_holdings ( backtest_id INTEGER REFERENCES backtests(id), rebalance_date DATE, ticker VARCHAR(20), name VARCHAR(100), weight DECIMAL(10,4), shares INTEGER, price DECIMAL(12,2), PRIMARY KEY (backtest_id, rebalance_date, ticker) ); ``` ### 1.5 backtest_transactions (거래 내역) ```sql CREATE TABLE backtest_transactions ( id SERIAL PRIMARY KEY, backtest_id INTEGER REFERENCES backtests(id), date DATE, ticker VARCHAR(20), action VARCHAR(10), -- buy/sell shares INTEGER, price DECIMAL(12,2), commission DECIMAL(12,2) ); ``` --- ## 2. API 엔드포인트 | Method | Endpoint | 설명 | |--------|----------|------| | POST | /api/backtest | 백테스트 생성 (작업 ID 반환) | | GET | /api/backtest | 내 백테스트 목록 조회 | | GET | /api/backtest/{id} | 백테스트 상태/결과 조회 | | GET | /api/backtest/{id}/equity-curve | 자산 추이 데이터 | | GET | /api/backtest/{id}/holdings | 리밸런싱별 보유 종목 | | GET | /api/backtest/{id}/transactions | 거래 내역 | | DELETE | /api/backtest/{id} | 백테스트 삭제 | ### 요청 예시 (POST /api/backtest) ```json { "strategy_type": "multi_factor", "strategy_params": { "weights": {"value": 0.3, "quality": 0.3, "momentum": 0.4}, "top_n": 30 }, "start_date": "2020-01-01", "end_date": "2024-12-31", "rebalance_period": "quarterly", "initial_capital": 100000000, "commission_rate": 0.00015, "slippage_rate": 0.001 } ``` ### 응답 예시 (GET /api/backtest/{id}) ```json { "id": 1, "status": "completed", "strategy_type": "multi_factor", "start_date": "2020-01-01", "end_date": "2024-12-31", "result": { "total_return": 125.4, "cagr": 17.8, "mdd": -32.5, "sharpe_ratio": 0.85, "volatility": 22.3, "benchmark_return": 45.2, "excess_return": 80.2 } } ``` --- ## 3. 서비스 구조 ``` backend/app/services/backtest/ ├── __init__.py ├── engine.py # 메인 백테스트 엔진 ├── portfolio.py # 가상 포트폴리오 관리 ├── metrics.py # 성과 지표 계산 └── worker.py # 백그라운드 작업 처리 ``` ### 3.1 BacktestEngine (engine.py) ```python class BacktestEngine: def __init__(self, db: Session): self.db = db def run(self, backtest_id: int) -> None: """백테스트 실행 메인 로직""" backtest = self._get_backtest(backtest_id) # 1. 리밸런싱 날짜 목록 생성 rebalance_dates = self._generate_rebalance_dates( backtest.start_date, backtest.end_date, backtest.rebalance_period ) # 2. 가상 포트폴리오 초기화 portfolio = VirtualPortfolio(backtest.initial_capital) # 3. 벤치마크 데이터 로드 benchmark_prices = self._load_benchmark_prices( backtest.benchmark, backtest.start_date, backtest.end_date ) # 4. 전략 인스턴스 생성 strategy = self._create_strategy( backtest.strategy_type, backtest.strategy_params ) # 5. 시뮬레이션 실행 equity_curve = [] holdings_history = [] transactions = [] for date in self._get_trading_days(backtest.start_date, backtest.end_date): prices = self._get_prices_for_date(date) # 리밸런싱 날짜인 경우 if date in rebalance_dates: # 전략 실행하여 종목 선정 targets = strategy.run(date) # 포트폴리오 리밸런싱 txns = portfolio.rebalance( targets, prices, backtest.commission_rate, backtest.slippage_rate ) transactions.extend(txns) # 보유 종목 기록 holdings_history.append({ 'date': date, 'holdings': portfolio.get_holdings_with_weights(prices) }) # 일별 가치 기록 equity_curve.append({ 'date': date, 'portfolio_value': portfolio.get_value(prices), 'benchmark_value': benchmark_prices.get(date) }) # 6. 성과 지표 계산 metrics = MetricsCalculator.calculate_all(equity_curve) # 7. 결과 저장 self._save_results(backtest_id, metrics, equity_curve, holdings_history, transactions) ``` ### 3.2 VirtualPortfolio (portfolio.py) ```python class VirtualPortfolio: def __init__(self, initial_capital: Decimal): self.cash = initial_capital self.holdings: Dict[str, int] = {} # ticker -> shares def rebalance( self, targets: List[StockFactor], prices: Dict[str, Decimal], commission_rate: Decimal, slippage_rate: Decimal ) -> List[Transaction]: """목표 비중으로 리밸런싱""" transactions = [] total_value = self.get_value(prices) # 목표 비중 계산 (동일 비중) target_weight = Decimal("1") / len(targets) target_tickers = {t.ticker for t in targets} # 매도: 목표에 없는 종목 for ticker in list(self.holdings.keys()): if ticker not in target_tickers: shares = self.holdings[ticker] price = prices[ticker] * (1 - slippage_rate) proceeds = shares * price commission = proceeds * commission_rate self.cash += proceeds - commission del self.holdings[ticker] transactions.append(Transaction( ticker=ticker, action='sell', shares=shares, price=price, commission=commission )) # 매수: 목표 비중에 맞게 for target in targets: ticker = target.ticker target_value = total_value * target_weight current_value = self.holdings.get(ticker, 0) * prices.get(ticker, 0) if target_value > current_value: buy_value = target_value - current_value price = prices[ticker] * (1 + slippage_rate) shares = int(buy_value / price) if shares > 0: cost = shares * price commission = cost * commission_rate if self.cash >= cost + commission: self.cash -= cost + commission self.holdings[ticker] = self.holdings.get(ticker, 0) + shares transactions.append(Transaction( ticker=ticker, action='buy', shares=shares, price=price, commission=commission )) return transactions def get_value(self, prices: Dict[str, Decimal]) -> Decimal: """총 포트폴리오 가치""" holdings_value = sum( shares * prices.get(ticker, Decimal("0")) for ticker, shares in self.holdings.items() ) return self.cash + holdings_value ``` ### 3.3 MetricsCalculator (metrics.py) ```python class MetricsCalculator: @staticmethod def calculate_all(equity_curve: List[dict]) -> BacktestMetrics: values = [e['portfolio_value'] for e in equity_curve] benchmark_values = [e['benchmark_value'] for e in equity_curve] returns = MetricsCalculator._calculate_returns(values) return BacktestMetrics( total_return=MetricsCalculator.calculate_total_return(values), cagr=MetricsCalculator.calculate_cagr(values, len(values) / 252), mdd=MetricsCalculator.calculate_mdd(values), sharpe_ratio=MetricsCalculator.calculate_sharpe(returns), volatility=MetricsCalculator.calculate_volatility(returns), benchmark_return=MetricsCalculator.calculate_total_return(benchmark_values), excess_return=... # total_return - benchmark_return ) @staticmethod def calculate_total_return(values: List[Decimal]) -> Decimal: return (values[-1] - values[0]) / values[0] * 100 @staticmethod def calculate_cagr(values: List[Decimal], years: float) -> Decimal: total_return = values[-1] / values[0] return (total_return ** (1 / years) - 1) * 100 @staticmethod def calculate_mdd(values: List[Decimal]) -> Decimal: peak = values[0] max_dd = Decimal("0") for value in values: if value > peak: peak = value dd = (peak - value) / peak * 100 if dd > max_dd: max_dd = dd return -max_dd @staticmethod def calculate_sharpe(returns: List[Decimal], risk_free: Decimal = Decimal("0.03")) -> Decimal: avg_return = sum(returns) / len(returns) * 252 std_return = ... # 표준편차 * sqrt(252) return (avg_return - risk_free) / std_return ``` ### 3.4 Worker (worker.py) ```python from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=2) def submit_backtest(backtest_id: int, db_url: str): """백테스트 작업 제출""" executor.submit(_run_backtest_job, backtest_id, db_url) def _run_backtest_job(backtest_id: int, db_url: str): """백그라운드에서 실행""" engine = create_engine(db_url) Session = sessionmaker(bind=engine) db = Session() try: # status → running backtest = db.query(Backtest).get(backtest_id) backtest.status = 'running' db.commit() # 백테스트 실행 engine = BacktestEngine(db) engine.run(backtest_id) # status → completed backtest.status = 'completed' backtest.completed_at = datetime.utcnow() db.commit() except Exception as e: backtest.status = 'failed' backtest.error_message = str(e) db.commit() finally: db.close() ``` --- ## 4. 프론트엔드 페이지 ### 4.1 /backtest (목록 + 생성) **새 백테스트 폼:** - 전략 선택 드롭다운 - 전략 파라미터 (전략별 동적 폼) - 기간 선택 (DatePicker) - 리밸런싱 주기 라디오 버튼 - 초기 자본금 입력 - 거래 비용 설정 (접이식 고급 옵션) - 실행 버튼 **백테스트 목록 테이블:** | 전략 | 기간 | 수익률 | CAGR | MDD | 상태 | 생성일 | ### 4.2 /backtest/[id] (결과 상세) **성과 요약 카드:** ``` ┌─────────────────────────────────────────────────────┐ │ 총 수익률 CAGR MDD 샤프 변동성 │ │ +125.4% 17.8% -32.5% 0.85 22.3% │ ├─────────────────────────────────────────────────────┤ │ 벤치마크 수익률: +45.2% 초과 수익률: +80.2% │ └─────────────────────────────────────────────────────┘ ``` **자산 추이 차트:** - 포트폴리오 라인 (파란색) - 벤치마크 라인 (회색) - 낙폭 영역 (빨간색, 아래 서브차트) **탭: 리밸런싱 이력 / 거래 내역** --- ## 5. 구현 태스크 ### Task 1: Backtest Pydantic 스키마 - `backend/app/schemas/backtest.py` 생성 - `backend/app/schemas/__init__.py` 업데이트 ### Task 2: Backtest SQLAlchemy 모델 - `backend/app/models/backtest.py` 생성 - `backend/app/models/__init__.py` 업데이트 - Alembic 마이그레이션 생성/실행 ### Task 3: MetricsCalculator 서비스 - `backend/app/services/backtest/metrics.py` 생성 ### Task 4: VirtualPortfolio 서비스 - `backend/app/services/backtest/portfolio.py` 생성 ### Task 5: BacktestEngine 서비스 - `backend/app/services/backtest/engine.py` 생성 - `backend/app/services/backtest/__init__.py` 생성 ### Task 6: Worker 서비스 - `backend/app/services/backtest/worker.py` 생성 ### Task 7: Backtest API 엔드포인트 - `backend/app/api/backtest.py` 생성 - `backend/app/api/__init__.py` 업데이트 - `backend/app/main.py` 업데이트 ### Task 8: Frontend 백테스트 목록/생성 페이지 - `frontend/src/app/backtest/page.tsx` 생성 ### Task 9: Frontend 백테스트 결과 페이지 - `frontend/src/app/backtest/[id]/page.tsx` 생성 ### Task 10: 통합 검증 - Frontend 빌드 확인 - Git 커밋 히스토리 확인 --- **문서 버전:** 1.0 **작성일:** 2026-02-03