diff --git a/backend/app/schemas/backtest.py b/backend/app/schemas/backtest.py index 546b217..6d057d4 100644 --- a/backend/app/schemas/backtest.py +++ b/backend/app/schemas/backtest.py @@ -8,6 +8,8 @@ from enum import Enum from pydantic import BaseModel, Field +from app.schemas.portfolio import FloatDecimal + class RebalancePeriod(str, Enum): MONTHLY = "monthly" @@ -39,13 +41,13 @@ class BacktestCreate(BaseModel): class BacktestMetrics(BaseModel): """Backtest result metrics.""" - total_return: Decimal - cagr: Decimal - mdd: Decimal - sharpe_ratio: Decimal - volatility: Decimal - benchmark_return: Decimal - excess_return: Decimal + total_return: FloatDecimal + cagr: FloatDecimal + mdd: FloatDecimal + sharpe_ratio: FloatDecimal + volatility: FloatDecimal + benchmark_return: FloatDecimal + excess_return: FloatDecimal class BacktestResponse(BaseModel): @@ -57,9 +59,9 @@ class BacktestResponse(BaseModel): start_date: date end_date: date rebalance_period: str - initial_capital: Decimal - commission_rate: Decimal - slippage_rate: Decimal + initial_capital: FloatDecimal + commission_rate: FloatDecimal + slippage_rate: FloatDecimal benchmark: str status: str created_at: datetime @@ -80,9 +82,9 @@ class BacktestListItem(BaseModel): rebalance_period: str status: str created_at: datetime - total_return: Optional[Decimal] = None - cagr: Optional[Decimal] = None - mdd: Optional[Decimal] = None + total_return: Optional[FloatDecimal] = None + cagr: Optional[FloatDecimal] = None + mdd: Optional[FloatDecimal] = None class Config: from_attributes = True @@ -91,9 +93,9 @@ class BacktestListItem(BaseModel): class EquityCurvePoint(BaseModel): """Single point in equity curve.""" date: date - portfolio_value: Decimal - benchmark_value: Decimal - drawdown: Decimal + portfolio_value: FloatDecimal + benchmark_value: FloatDecimal + drawdown: FloatDecimal class Config: from_attributes = True @@ -103,9 +105,9 @@ class HoldingItem(BaseModel): """Single holding at a rebalance date.""" ticker: str name: str - weight: Decimal + weight: FloatDecimal shares: int - price: Decimal + price: FloatDecimal class RebalanceHoldings(BaseModel): @@ -121,8 +123,8 @@ class TransactionItem(BaseModel): ticker: str action: str shares: int - price: Decimal - commission: Decimal + price: FloatDecimal + commission: FloatDecimal class Config: from_attributes = True diff --git a/backend/app/schemas/portfolio.py b/backend/app/schemas/portfolio.py index 5ae8eb4..0762dce 100644 --- a/backend/app/schemas/portfolio.py +++ b/backend/app/schemas/portfolio.py @@ -3,15 +3,21 @@ Portfolio related Pydantic schemas. """ from datetime import datetime, date from decimal import Decimal -from typing import Optional, List +from typing import Annotated, Optional, List -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, PlainSerializer + +# Decimal type that serializes as float in JSON responses (not string) +FloatDecimal = Annotated[ + Decimal, + PlainSerializer(lambda v: float(v), return_type=float, when_used='json'), +] # Target schemas class TargetBase(BaseModel): ticker: str - target_ratio: Decimal = Field(..., ge=0, le=100) + target_ratio: FloatDecimal = Field(..., ge=0, le=100) class TargetCreate(TargetBase): @@ -27,7 +33,7 @@ class TargetResponse(TargetBase): class HoldingBase(BaseModel): ticker: str quantity: int = Field(..., ge=0) - avg_price: Decimal = Field(..., ge=0) + avg_price: FloatDecimal = Field(..., ge=0) class HoldingCreate(HoldingBase): @@ -41,11 +47,11 @@ class HoldingResponse(HoldingBase): class HoldingWithValue(HoldingResponse): """Holding with calculated values.""" - current_price: Decimal | None = None - value: Decimal | None = None - current_ratio: Decimal | None = None - profit_loss: Decimal | None = None - profit_loss_ratio: Decimal | None = None + current_price: FloatDecimal | None = None + value: FloatDecimal | None = None + current_ratio: FloatDecimal | None = None + profit_loss: FloatDecimal | None = None + profit_loss_ratio: FloatDecimal | None = None # Transaction schemas @@ -53,7 +59,7 @@ class TransactionBase(BaseModel): ticker: str tx_type: str # "buy" or "sell" quantity: int = Field(..., gt=0) - price: Decimal = Field(..., gt=0) + price: FloatDecimal = Field(..., gt=0) executed_at: datetime memo: Optional[str] = None @@ -98,18 +104,18 @@ class PortfolioDetail(PortfolioResponse): """Portfolio with targets and holdings.""" targets: List[TargetResponse] = [] holdings: List[HoldingWithValue] = [] - total_value: Decimal | None = None - total_invested: Decimal | None = None - total_profit_loss: Decimal | None = None + total_value: FloatDecimal | None = None + total_invested: FloatDecimal | None = None + total_profit_loss: FloatDecimal | None = None # Snapshot schemas class SnapshotHoldingResponse(BaseModel): ticker: str quantity: int - price: Decimal - value: Decimal - current_ratio: Decimal + price: FloatDecimal + value: FloatDecimal + current_ratio: FloatDecimal class Config: from_attributes = True @@ -119,7 +125,7 @@ class SnapshotListItem(BaseModel): """Snapshot list item (without holdings).""" id: int portfolio_id: int - total_value: Decimal + total_value: FloatDecimal snapshot_date: date class Config: @@ -130,7 +136,7 @@ class SnapshotResponse(BaseModel): """Snapshot detail with holdings.""" id: int portfolio_id: int - total_value: Decimal + total_value: FloatDecimal snapshot_date: date holdings: List[SnapshotHoldingResponse] = [] @@ -141,9 +147,9 @@ class SnapshotResponse(BaseModel): class ReturnDataPoint(BaseModel): """Single data point for returns chart.""" date: date - total_value: Decimal - daily_return: Decimal | None = None - cumulative_return: Decimal | None = None + total_value: FloatDecimal + daily_return: FloatDecimal | None = None + cumulative_return: FloatDecimal | None = None class ReturnsResponse(BaseModel): @@ -151,8 +157,8 @@ class ReturnsResponse(BaseModel): portfolio_id: int start_date: date | None = None end_date: date | None = None - total_return: Decimal | None = None - cagr: Decimal | None = None + total_return: FloatDecimal | None = None + cagr: FloatDecimal | None = None data: List[ReturnDataPoint] = [] @@ -160,19 +166,19 @@ class ReturnsResponse(BaseModel): class RebalanceItem(BaseModel): ticker: str name: str | None = None - target_ratio: Decimal - current_ratio: Decimal + target_ratio: FloatDecimal + current_ratio: FloatDecimal current_quantity: int - current_value: Decimal - target_value: Decimal - diff_value: Decimal + current_value: FloatDecimal + target_value: FloatDecimal + diff_value: FloatDecimal diff_quantity: int action: str # "buy", "sell", or "hold" class RebalanceResponse(BaseModel): portfolio_id: int - total_value: Decimal + total_value: FloatDecimal items: List[RebalanceItem] @@ -182,9 +188,9 @@ class RebalanceSimulationRequest(BaseModel): class RebalanceSimulationResponse(BaseModel): portfolio_id: int - current_total: Decimal - additional_amount: Decimal - new_total: Decimal + current_total: FloatDecimal + additional_amount: FloatDecimal + new_total: FloatDecimal items: List[RebalanceItem] @@ -199,22 +205,22 @@ class RebalanceCalculateItem(BaseModel): """Extended rebalance item with price change info.""" ticker: str name: str | None = None - target_ratio: Decimal - current_ratio: Decimal + target_ratio: FloatDecimal + current_ratio: FloatDecimal current_quantity: int - current_value: Decimal - current_price: Decimal - target_value: Decimal - diff_ratio: Decimal + current_value: FloatDecimal + current_price: FloatDecimal + target_value: FloatDecimal + diff_ratio: FloatDecimal diff_quantity: int action: str # "buy", "sell", or "hold" - change_vs_prev_month: Decimal | None = None - change_vs_start: Decimal | None = None + change_vs_prev_month: FloatDecimal | None = None + change_vs_start: FloatDecimal | None = None class RebalanceCalculateResponse(BaseModel): """Response for manual rebalance calculation.""" portfolio_id: int - total_assets: Decimal - available_to_buy: Decimal | None = None + total_assets: FloatDecimal + available_to_buy: FloatDecimal | None = None items: List[RebalanceCalculateItem] diff --git a/backend/app/schemas/strategy.py b/backend/app/schemas/strategy.py index 23de58f..1646c57 100644 --- a/backend/app/schemas/strategy.py +++ b/backend/app/schemas/strategy.py @@ -7,13 +7,15 @@ from typing import Optional, List, Dict from pydantic import BaseModel, Field +from app.schemas.portfolio import FloatDecimal + class FactorWeights(BaseModel): """Factor weights for multi-factor strategy.""" - value: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) - quality: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) - momentum: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) - low_vol: Decimal = Field(default=Decimal("0.25"), ge=0, le=1) + value: FloatDecimal = Field(default=Decimal("0.25"), ge=0, le=1) + quality: FloatDecimal = Field(default=Decimal("0.25"), ge=0, le=1) + momentum: FloatDecimal = Field(default=Decimal("0.25"), ge=0, le=1) + low_vol: FloatDecimal = Field(default=Decimal("0.25"), ge=0, le=1) class UniverseFilter(BaseModel): @@ -44,8 +46,8 @@ class QualityRequest(StrategyRequest): class ValueMomentumRequest(StrategyRequest): """Value-Momentum strategy request.""" - value_weight: Decimal = Field(default=Decimal("0.5"), ge=0, le=1) - momentum_weight: Decimal = Field(default=Decimal("0.5"), ge=0, le=1) + value_weight: FloatDecimal = Field(default=Decimal("0.5"), ge=0, le=1) + momentum_weight: FloatDecimal = Field(default=Decimal("0.5"), ge=0, le=1) class StockFactor(BaseModel): @@ -55,23 +57,23 @@ class StockFactor(BaseModel): market: str sector_name: Optional[str] = None market_cap: Optional[int] = None - close_price: Optional[Decimal] = None + close_price: Optional[FloatDecimal] = None # Raw metrics - per: Optional[Decimal] = None - pbr: Optional[Decimal] = None - psr: Optional[Decimal] = None - pcr: Optional[Decimal] = None - dividend_yield: Optional[Decimal] = None - roe: Optional[Decimal] = None + per: Optional[FloatDecimal] = None + pbr: Optional[FloatDecimal] = None + psr: Optional[FloatDecimal] = None + pcr: Optional[FloatDecimal] = None + dividend_yield: Optional[FloatDecimal] = None + roe: Optional[FloatDecimal] = None # Factor scores (z-scores) - value_score: Optional[Decimal] = None - quality_score: Optional[Decimal] = None - momentum_score: Optional[Decimal] = None + value_score: Optional[FloatDecimal] = None + quality_score: Optional[FloatDecimal] = None + momentum_score: Optional[FloatDecimal] = None # Composite - total_score: Optional[Decimal] = None + total_score: Optional[FloatDecimal] = None rank: Optional[int] = None fscore: Optional[int] = None @@ -92,19 +94,19 @@ class StockInfo(BaseModel): market: str sector_name: Optional[str] = None stock_type: str - close_price: Optional[Decimal] = None + close_price: Optional[FloatDecimal] = None market_cap: Optional[int] = None # Valuation - per: Optional[Decimal] = None - pbr: Optional[Decimal] = None - psr: Optional[Decimal] = None - pcr: Optional[Decimal] = None - dividend_yield: Optional[Decimal] = None + per: Optional[FloatDecimal] = None + pbr: Optional[FloatDecimal] = None + psr: Optional[FloatDecimal] = None + pcr: Optional[FloatDecimal] = None + dividend_yield: Optional[FloatDecimal] = None # Per-share data - eps: Optional[Decimal] = None - bps: Optional[Decimal] = None + eps: Optional[FloatDecimal] = None + bps: Optional[FloatDecimal] = None base_date: Optional[date] = None @@ -122,10 +124,10 @@ class StockSearchResult(BaseModel): class PriceData(BaseModel): """Price data point.""" date: date - open: Decimal - high: Decimal - low: Decimal - close: Decimal + open: FloatDecimal + high: FloatDecimal + low: FloatDecimal + close: FloatDecimal volume: int class Config: diff --git a/frontend/src/app/portfolio/[id]/page.tsx b/frontend/src/app/portfolio/[id]/page.tsx index c5140f6..1394428 100644 --- a/frontend/src/app/portfolio/[id]/page.tsx +++ b/frontend/src/app/portfolio/[id]/page.tsx @@ -10,7 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { TradingViewChart } from '@/components/charts/trading-view-chart'; import { DonutChart } from '@/components/charts/donut-chart'; import { api } from '@/lib/api'; -import { AreaData, Time } from 'lightweight-charts'; +import type { AreaData, Time } from 'lightweight-charts'; interface HoldingWithValue { ticker: string; @@ -31,10 +31,10 @@ interface Target { interface Transaction { id: number; ticker: string; - transaction_type: string; + tx_type: string; quantity: number; price: number; - created_at: string; + executed_at: string; } interface PortfolioDetail { @@ -432,18 +432,18 @@ export default function PortfolioDetailPage() { {transactions.map((tx) => ( - {new Date(tx.created_at).toLocaleString('ko-KR')} + {new Date(tx.executed_at).toLocaleString('ko-KR')} {tx.ticker} - {tx.transaction_type === 'buy' ? '매수' : '매도'} + {tx.tx_type === 'buy' ? '매수' : '매도'}