119 lines
3.3 KiB
Python
119 lines
3.3 KiB
Python
|
|
"""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
|