머니페니 f818bd3290 feat: add walk-forward analysis for backtests
- Add WalkForwardResult model with train/test window metrics
- Create WalkForwardEngine that reuses existing BacktestEngine
  with rolling train/test window splits
- Add POST/GET /api/backtest/{id}/walkforward endpoints
- Add Walk-forward tab to backtest detail page with parameter
  controls, cumulative return chart, and window results table
- Add Alembic migration for walkforward_results table
- Add 8 unit tests for window generation logic (100 total passed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 22:33:41 +09:00

160 lines
4.0 KiB
Python

"""
Backtest related Pydantic schemas.
"""
from datetime import date, datetime
from decimal import Decimal
from typing import Optional, List, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field
from app.schemas.portfolio import FloatDecimal
class RebalancePeriod(str, Enum):
MONTHLY = "monthly"
QUARTERLY = "quarterly"
SEMI_ANNUAL = "semi_annual"
ANNUAL = "annual"
class BacktestStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class BacktestCreate(BaseModel):
"""Request to create a new backtest."""
strategy_type: str = Field(..., description="multi_factor, quality, value_momentum, or kjb")
strategy_params: Dict[str, Any] = Field(default_factory=dict)
start_date: date
end_date: date
rebalance_period: RebalancePeriod = RebalancePeriod.QUARTERLY
initial_capital: Decimal = Field(default=Decimal("100000000"), gt=0)
commission_rate: Decimal = Field(default=Decimal("0.00015"), ge=0, le=1)
slippage_rate: Decimal = Field(default=Decimal("0.001"), ge=0, le=1)
benchmark: str = Field(default="KOSPI")
top_n: int = Field(default=30, ge=1, le=100)
class BacktestMetrics(BaseModel):
"""Backtest result metrics."""
total_return: FloatDecimal
cagr: FloatDecimal
mdd: FloatDecimal
sharpe_ratio: FloatDecimal
volatility: FloatDecimal
benchmark_return: FloatDecimal
excess_return: FloatDecimal
class BacktestResponse(BaseModel):
"""Backtest response with status and optional results."""
id: int
user_id: int
strategy_type: str
strategy_params: Dict[str, Any]
start_date: date
end_date: date
rebalance_period: str
initial_capital: FloatDecimal
commission_rate: FloatDecimal
slippage_rate: FloatDecimal
benchmark: str
status: str
created_at: datetime
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
result: Optional[BacktestMetrics] = None
class Config:
from_attributes = True
class BacktestListItem(BaseModel):
"""Backtest item for list view."""
id: int
strategy_type: str
start_date: date
end_date: date
rebalance_period: str
status: str
created_at: datetime
total_return: Optional[FloatDecimal] = None
cagr: Optional[FloatDecimal] = None
mdd: Optional[FloatDecimal] = None
class Config:
from_attributes = True
class EquityCurvePoint(BaseModel):
"""Single point in equity curve."""
date: date
portfolio_value: FloatDecimal
benchmark_value: FloatDecimal
drawdown: FloatDecimal
class Config:
from_attributes = True
class HoldingItem(BaseModel):
"""Single holding at a rebalance date."""
ticker: str
name: str
weight: FloatDecimal
shares: int
price: FloatDecimal
class RebalanceHoldings(BaseModel):
"""Holdings at a specific rebalance date."""
rebalance_date: date
holdings: List[HoldingItem]
class TransactionItem(BaseModel):
"""Single transaction."""
id: int
date: date
ticker: str
name: str | None = None
action: str
shares: int
price: FloatDecimal
commission: FloatDecimal
class Config:
from_attributes = True
class WalkForwardRequest(BaseModel):
"""Request to run walk-forward analysis."""
train_months: int = Field(default=12, ge=3, le=60)
test_months: int = Field(default=3, ge=1, le=24)
step_months: int = Field(default=3, ge=1, le=24)
class WalkForwardWindowResult(BaseModel):
"""Single walk-forward window result."""
window_index: int
train_start: date
train_end: date
test_start: date
test_end: date
test_return: FloatDecimal | None = None
test_sharpe: FloatDecimal | None = None
test_mdd: FloatDecimal | None = None
class Config:
from_attributes = True
class WalkForwardResponse(BaseModel):
"""Walk-forward analysis results."""
backtest_id: int
windows: List[WalkForwardWindowResult]