penti/backend/app/schemas/portfolio.py

119 lines
3.3 KiB
Python
Raw Permalink Normal View History

2026-01-31 23:30:51 +09:00
"""Portfolio schemas."""
from pydantic import BaseModel, Field, validator
from typing import List, Dict, Optional
from datetime import datetime
from uuid import UUID
class PortfolioAssetCreate(BaseModel):
"""포트폴리오 자산 생성 요청."""
ticker: str = Field(..., description="종목코드")
target_ratio: float = Field(..., ge=0, le=100, description="목표 비율 (%)")
class PortfolioAssetResponse(BaseModel):
"""포트폴리오 자산 응답."""
id: UUID
ticker: str
target_ratio: float
class Config:
from_attributes = True
class PortfolioCreate(BaseModel):
"""포트폴리오 생성 요청."""
name: str = Field(..., min_length=1, max_length=100, description="포트폴리오 이름")
description: Optional[str] = Field(None, description="포트폴리오 설명")
assets: List[PortfolioAssetCreate] = Field(..., min_items=1, description="자산 목록")
@validator('assets')
def validate_total_ratio(cls, v):
"""목표 비율 합계가 100%인지 검증."""
total = sum(asset.target_ratio for asset in v)
if abs(total - 100.0) > 0.01: # 부동소수점 오차 허용
raise ValueError(f'목표 비율의 합은 100%여야 합니다 (현재: {total}%)')
return v
class PortfolioUpdate(BaseModel):
"""포트폴리오 수정 요청."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = None
assets: Optional[List[PortfolioAssetCreate]] = None
@validator('assets')
def validate_total_ratio(cls, v):
"""목표 비율 합계가 100%인지 검증."""
if v is not None:
total = sum(asset.target_ratio for asset in v)
if abs(total - 100.0) > 0.01:
raise ValueError(f'목표 비율의 합은 100%여야 합니다 (현재: {total}%)')
return v
class PortfolioResponse(BaseModel):
"""포트폴리오 응답."""
id: UUID
name: str
description: Optional[str]
user_id: Optional[str]
assets: List[PortfolioAssetResponse]
created_at: datetime
class Config:
from_attributes = True
class CurrentHolding(BaseModel):
"""현재 보유 자산."""
ticker: str = Field(..., description="종목코드")
quantity: float = Field(..., ge=0, description="보유 수량")
class RebalancingRequest(BaseModel):
"""리밸런싱 요청."""
portfolio_id: UUID = Field(..., description="포트폴리오 ID")
current_holdings: List[CurrentHolding] = Field(..., description="현재 보유 자산")
cash: float = Field(default=0, ge=0, description="현금 (원)")
class RebalancingRecommendation(BaseModel):
"""리밸런싱 추천."""
ticker: str
name: str
current_quantity: float
current_value: float
current_ratio: float
target_ratio: float
target_value: float
delta_value: float
delta_quantity: float
action: str # 'buy', 'sell', 'hold'
current_price: float
class RebalancingResponse(BaseModel):
"""리밸런싱 응답."""
portfolio: PortfolioResponse
total_value: float
cash: float
recommendations: List[RebalancingRecommendation]
summary: Dict[str, int] # {'buy': N, 'sell': M, 'hold': K}
class PortfolioListResponse(BaseModel):
"""포트폴리오 목록 응답."""
items: List[PortfolioResponse]
total: int