From a3d9819175b0c8ea46613ee7bb9b281e6640436d Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Tue, 3 Feb 2026 07:06:22 +0900 Subject: [PATCH] feat: add portfolio Pydantic schemas --- backend/app/schemas/__init__.py | 16 ++++ backend/app/schemas/portfolio.py | 158 +++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 backend/app/schemas/portfolio.py diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 13faa3f..69ed0e3 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,5 +1,14 @@ from app.schemas.user import UserBase, UserCreate, UserResponse from app.schemas.auth import Token, TokenPayload, LoginRequest +from app.schemas.portfolio import ( + TargetCreate, TargetResponse, + HoldingCreate, HoldingResponse, HoldingWithValue, + TransactionCreate, TransactionResponse, + PortfolioCreate, PortfolioUpdate, PortfolioResponse, PortfolioDetail, + SnapshotResponse, SnapshotHoldingResponse, + RebalanceItem, RebalanceResponse, + RebalanceSimulationRequest, RebalanceSimulationResponse, +) __all__ = [ "UserBase", @@ -8,4 +17,11 @@ __all__ = [ "Token", "TokenPayload", "LoginRequest", + "TargetCreate", "TargetResponse", + "HoldingCreate", "HoldingResponse", "HoldingWithValue", + "TransactionCreate", "TransactionResponse", + "PortfolioCreate", "PortfolioUpdate", "PortfolioResponse", "PortfolioDetail", + "SnapshotResponse", "SnapshotHoldingResponse", + "RebalanceItem", "RebalanceResponse", + "RebalanceSimulationRequest", "RebalanceSimulationResponse", ] diff --git a/backend/app/schemas/portfolio.py b/backend/app/schemas/portfolio.py new file mode 100644 index 0000000..d166fee --- /dev/null +++ b/backend/app/schemas/portfolio.py @@ -0,0 +1,158 @@ +""" +Portfolio related Pydantic schemas. +""" +from datetime import datetime, date +from decimal import Decimal +from typing import Optional, List + +from pydantic import BaseModel, Field + + +# Target schemas +class TargetBase(BaseModel): + ticker: str + target_ratio: Decimal = Field(..., ge=0, le=100) + + +class TargetCreate(TargetBase): + pass + + +class TargetResponse(TargetBase): + class Config: + from_attributes = True + + +# Holding schemas +class HoldingBase(BaseModel): + ticker: str + quantity: int = Field(..., ge=0) + avg_price: Decimal = Field(..., ge=0) + + +class HoldingCreate(HoldingBase): + pass + + +class HoldingResponse(HoldingBase): + class Config: + from_attributes = True + + +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 + + +# Transaction schemas +class TransactionBase(BaseModel): + ticker: str + tx_type: str # "buy" or "sell" + quantity: int = Field(..., gt=0) + price: Decimal = Field(..., gt=0) + executed_at: datetime + memo: Optional[str] = None + + +class TransactionCreate(TransactionBase): + pass + + +class TransactionResponse(TransactionBase): + id: int + + class Config: + from_attributes = True + + +# Portfolio schemas +class PortfolioBase(BaseModel): + name: str = Field(..., min_length=1, max_length=100) + portfolio_type: str = "general" # "pension" or "general" + + +class PortfolioCreate(PortfolioBase): + pass + + +class PortfolioUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=100) + portfolio_type: Optional[str] = None + + +class PortfolioResponse(PortfolioBase): + id: int + user_id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +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 + + +# Snapshot schemas +class SnapshotHoldingResponse(BaseModel): + ticker: str + quantity: int + price: Decimal + value: Decimal + current_ratio: Decimal + + class Config: + from_attributes = True + + +class SnapshotResponse(BaseModel): + id: int + portfolio_id: int + total_value: Decimal + snapshot_date: date + holdings: List[SnapshotHoldingResponse] = [] + + class Config: + from_attributes = True + + +# Rebalancing schemas +class RebalanceItem(BaseModel): + ticker: str + name: str | None = None + target_ratio: Decimal + current_ratio: Decimal + current_quantity: int + current_value: Decimal + target_value: Decimal + diff_value: Decimal + diff_quantity: int + action: str # "buy", "sell", or "hold" + + +class RebalanceResponse(BaseModel): + portfolio_id: int + total_value: Decimal + items: List[RebalanceItem] + + +class RebalanceSimulationRequest(BaseModel): + additional_amount: Decimal = Field(..., gt=0) + + +class RebalanceSimulationResponse(BaseModel): + portfolio_id: int + current_total: Decimal + additional_amount: Decimal + new_total: Decimal + items: List[RebalanceItem]