All checks were successful
Deploy to Production / deploy (push) Successful in 1m33s
Pydantic v2's model_dump(mode="json") serializes Decimal as strings (e.g., "33.33" instead of 33.33), causing frontend crashes when calling .toFixed() on string values. Introduced FloatDecimal type alias with PlainSerializer to ensure Decimal fields are serialized as floats in JSON responses. Also fixed frontend Transaction interface to match backend field names (created_at → executed_at, transaction_type → tx_type). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
3.2 KiB
Python
131 lines
3.2 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, 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: 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
|
|
action: str
|
|
shares: int
|
|
price: FloatDecimal
|
|
commission: FloatDecimal
|
|
|
|
class Config:
|
|
from_attributes = True
|