diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 54b043c..204d513 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -15,6 +15,11 @@ from app.schemas.strategy import ( StockFactor, StrategyResult, StockInfo, StockSearchResult, PriceData, ) +from app.schemas.backtest import ( + RebalancePeriod, BacktestStatus, + BacktestCreate, BacktestMetrics, BacktestResponse, BacktestListItem, + EquityCurvePoint, HoldingItem, RebalanceHoldings, TransactionItem, +) __all__ = [ "UserBase", @@ -34,4 +39,7 @@ __all__ = [ "StrategyRequest", "MultiFactorRequest", "QualityRequest", "ValueMomentumRequest", "StockFactor", "StrategyResult", "StockInfo", "StockSearchResult", "PriceData", + "RebalancePeriod", "BacktestStatus", + "BacktestCreate", "BacktestMetrics", "BacktestResponse", "BacktestListItem", + "EquityCurvePoint", "HoldingItem", "RebalanceHoldings", "TransactionItem", ] diff --git a/backend/app/schemas/backtest.py b/backend/app/schemas/backtest.py new file mode 100644 index 0000000..546b217 --- /dev/null +++ b/backend/app/schemas/backtest.py @@ -0,0 +1,128 @@ +""" +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 + + +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, or value_momentum") + 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: Decimal + cagr: Decimal + mdd: Decimal + sharpe_ratio: Decimal + volatility: Decimal + benchmark_return: Decimal + excess_return: Decimal + + +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: Decimal + commission_rate: Decimal + slippage_rate: Decimal + 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[Decimal] = None + cagr: Optional[Decimal] = None + mdd: Optional[Decimal] = None + + class Config: + from_attributes = True + + +class EquityCurvePoint(BaseModel): + """Single point in equity curve.""" + date: date + portfolio_value: Decimal + benchmark_value: Decimal + drawdown: Decimal + + class Config: + from_attributes = True + + +class HoldingItem(BaseModel): + """Single holding at a rebalance date.""" + ticker: str + name: str + weight: Decimal + shares: int + price: Decimal + + +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 + action: str + shares: int + price: Decimal + commission: Decimal + + class Config: + from_attributes = True