# schemas.py from pydantic import BaseModel, Field, ConfigDict # Use ConfigDict for Pydantic V2 from typing import Optional, List from datetime import date, datetime from decimal import Decimal # --- Portfolio Schemas --- class PortfolioBase(BaseModel): id: int name: str description: Optional[str] = None is_active: bool created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) class PortfolioCreate(BaseModel): name: str = Field( ..., min_length=1, max_length=100, description="Name of the portfolio" ) description: Optional[str] = Field( None, description="Optional description for the portfolio" ) class PortfolioUpdate(BaseModel): name: Optional[str] = Field( None, min_length=1, max_length=100, description="New name for the portfolio" ) description: Optional[str] = Field( None, description="New description for the portfolio" ) is_active: Optional[bool] = Field( None, description="Set portfolio active or inactive status" ) class PortfolioListResponse(BaseModel): portfolios: List[PortfolioBase] total_count: int model_config = ConfigDict(from_attributes=True) # --- Stock Holding Schemas --- class StockHoldingBase(BaseModel): stock_id: int = Field(..., description="Internal ID of the stock master record") quantity: Decimal = Field(..., gt=0, description="Number of shares held") purchase_price: Decimal = Field( ..., gt=0, description="Average price per share at purchase for the aggregated holding", ) purchase_date: date = Field( ..., description="Representative date of stock purchase (e.g., latest buy)" ) notes: Optional[str] = Field(None, description="Additional notes for this holding") class StockHoldingCreate(StockHoldingBase): # Used when adding a new lot of stocks. purchase_price is unit price for this lot. pass class StockHoldingUpdate(BaseModel): # For updating notes or other specific fields on an aggregated holding. # Avoid direct updates to quantity/purchase_price here unless specific logic handles recalculation of average price. quantity: Optional[Decimal] = Field( None, gt=0, description="Updated total number of shares (use with caution)" ) purchase_price: Optional[Decimal] = Field( None, gt=0, description="Updated average purchase price per share (use with caution)", ) purchase_date: Optional[date] = Field( None, description="Updated representative purchase date" ) notes: Optional[str] = Field(None, description="Updated notes") class StockHoldingResponse(StockHoldingBase): id: int = Field( ..., description="Unique ID of the PortfolioStock (aggregated holding) record" ) stock_symbol: str = Field(..., description="Ticker symbol of the stock") stock_name: str = Field(..., description="Name of the stock company") current_price: Optional[Decimal] = Field( None, description="Current market price per share" ) market_value: Optional[Decimal] = Field( None, description="Total current market value of the holding" ) gain_loss: Optional[Decimal] = Field(None, description="Absolute gain or loss") gain_loss_percentage: Optional[Decimal] = Field( None, description="Percentage gain or loss" ) created_at: datetime model_config = ConfigDict(from_attributes=True) class StockSellSchema(BaseModel): quantity: Decimal = Field(..., gt=0, description="Number of shares to sell") sell_price: Decimal = Field( ..., gt=0, description="Price per share at which stock was sold" ) sell_date: date = Field(..., description="Date of the sale") notes: Optional[str] = Field( None, description="Additional notes for the sell transaction" ) # --- UTT (Unit Trust / Mutual Fund) Holding Schemas --- class UTTHoldingBase(BaseModel): utt_fund_id: int = Field( ..., description="Internal ID of the UTT fund master record" ) units_held: Decimal = Field(..., gt=0, description="Number of units held") purchase_price: Decimal = Field( ..., gt=0, description="Average price per unit at purchase (NAV) for the aggregated holding", ) purchase_date: date = Field( ..., description="Representative date of UTT purchase (e.g., latest buy)" ) notes: Optional[str] = Field(None, description="Additional notes for this holding") class UTTHoldingCreate(UTTHoldingBase): # Used when adding a new lot of UTTs. purchase_price is unit price for this lot. pass class UTTHoldingUpdate(BaseModel): units_held: Optional[Decimal] = Field( None, gt=0, description="Updated number of units held (use with caution)" ) purchase_price: Optional[Decimal] = Field( None, gt=0, description="Updated average purchase price per unit (use with caution)", ) purchase_date: Optional[date] = Field( None, description="Updated representative purchase date" ) notes: Optional[str] = Field(None, description="Updated notes") class UTTHoldingResponse(UTTHoldingBase): id: int = Field( ..., description="Unique ID of the PortfolioUTT (aggregated holding) record" ) fund_symbol: str = Field(..., description="Symbol of the UTT fund") fund_name: str = Field(..., description="Name of the UTT fund") current_nav: Optional[Decimal] = Field( None, description="Current Net Asset Value (NAV) per unit" ) market_value: Optional[Decimal] = Field( None, description="Total current market value of the holding" ) gain_loss: Optional[Decimal] = Field(None, description="Absolute gain or loss") gain_loss_percentage: Optional[Decimal] = Field( None, description="Percentage gain or loss" ) created_at: datetime model_config = ConfigDict(from_attributes=True) class UTTSellSchema(BaseModel): units_to_sell: Decimal = Field( ..., gt=0, description="Number of UTT units to sell" ) # Changed from 'units' sell_price: Decimal = Field( ..., gt=0, description="Price per unit at which UTT was sold (NAV)" ) sell_date: date = Field(..., description="Date of the sale") notes: Optional[str] = Field( None, description="Additional notes for the sell transaction" ) # --- Bond Holding Schemas --- class BondHoldingBase(BaseModel): # bond_id: int = Field(..., description="Internal ID of the bond master record") face_value_held: Decimal = Field( ..., gt=0, description="Total face value of the bond held" ) auction_number: Optional[int] = Field( None, description="Auction number if applicable (e.g., for government bonds)" ) auction_date: Optional[date] = Field( None, description="Auction date if applicable (e.g., for government bonds)" ) purchase_price: Decimal = Field( ..., gt=0, description="TOTAL purchase price paid for the entire face_value_held (aggregated holding).", ) purchase_date: date = Field( ..., description="Representative date of bond purchase (e.g., latest buy)" ) notes: Optional[str] = Field(None, description="Additional notes for this holding") class BondHoldingCreate(BondHoldingBase): # Used when adding a new lot of bonds. purchase_price is TOTAL cost for this specific lot of face_value_held. pass class BondHoldingUpdate(BaseModel): face_value_held: Optional[Decimal] = Field( None, gt=0, description="Updated total face value held" ) purchase_price: Optional[Decimal] = Field( None, gt=0, description="Updated TOTAL purchase price for the new face_value_held (use with caution)", ) purchase_date: Optional[date] = Field( None, description="Updated representative purchase date" ) notes: Optional[str] = Field(None, description="Updated notes") class BondHoldingResponse(BondHoldingBase): id: int = Field( ..., description="Unique ID of the PortfolioBond (aggregated holding) record" ) instrument_type: str = Field(..., description="Type of bond instrument") auction_number: Optional[int] = Field( None, description="Auction number if applicable" ) maturity_date: date = Field(..., description="Maturity date of the bond") current_price: Optional[Decimal] = Field( None, description="Current market price (e.g., percentage of face value like 99.5)", ) market_value: Optional[Decimal] = Field( None, description="Total current market value of the holding" ) accrued_interest: Optional[Decimal] = Field( None, description="Accrued interest on the bond" ) yield_to_maturity: Optional[Decimal] = Field( None, description="Yield to maturity of the bond" ) gain_loss: Optional[Decimal] = Field( None, description="Absolute gain or loss on principal" ) created_at: datetime model_config = ConfigDict(from_attributes=True) class BondSellSchema(BaseModel): face_value_to_sell: Decimal = Field( ..., gt=0, description="Face value of the bond portion being sold" ) # Changed from 'face_value_sold' sell_price: Decimal = Field( ..., gt=0, description="TOTAL selling proceeds for the face_value_to_sell." ) sell_date: date = Field(..., description="Date of the sale") notes: Optional[str] = Field( None, description="Additional notes for the sell transaction" ) # --- Calendar Event Schemas --- class CalendarEventBase(BaseModel): event_date: date event_type: str = Field(..., max_length=50) title: str = Field(..., max_length=200) description: Optional[str] = None asset_type: Optional[str] = Field(None, max_length=10) asset_id: Optional[int] = None estimated_amount: Optional[Decimal] = None class CalendarEventCreate(CalendarEventBase): pass class CalendarEventResponse(CalendarEventBase): id: int is_completed: bool = Field(False) created_at: datetime model_config = ConfigDict(from_attributes=True) # --- Transaction Schemas --- class TransactionBase(BaseModel): transaction_type: str = Field(..., max_length=20) asset_type: str = Field(..., max_length=10) asset_id: Optional[int] = None asset_name: Optional[str] = Field(None, max_length=100) quantity: Optional[Decimal] = None price: Optional[Decimal] = Field(None, ge=0) transaction_date: date notes: Optional[str] = None class TransactionCreate(TransactionBase): total_amount: Decimal # Service layer calculates and provides this. class TransactionResponse(TransactionBase): id: int total_amount: Decimal created_at: datetime model_config = ConfigDict(from_attributes=True) # --- Portfolio Analytics & Summary Schemas --- class AssetAllocation(BaseModel): stocks_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100) bonds_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100) utts_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100) cash_percentage: Decimal = Field(Decimal("0.0"), ge=0, le=100) total_value: Decimal model_config = ConfigDict(from_attributes=True) class CalendarEventResponse(BaseModel): event_date: date event_type: str # e.g., "Dividend Payment", "Bond Coupon" asset_symbol: str asset_name: str estimated_amount: Decimal notes: Optional[str] = None model_config = ConfigDict( from_attributes=True, ) class TransactionDetailResponse(BaseModel): id: int transaction_type: str asset_type: str asset_id: int quantity: Decimal price: Decimal total_amount: Decimal transaction_date: date notes: Optional[str] = None created_at: datetime # New fields to be added asset_name: Optional[str] = None asset_symbol: Optional[str] = None model_config = ConfigDict( from_attributes=True, ) class PortfolioSummary(BaseModel): portfolio: PortfolioBase total_market_value: Decimal total_cost_basis: Decimal overall_unrealized_gain_loss: Decimal overall_unrealized_gain_loss_percentage: Decimal stock_holdings: List[StockHoldingResponse] = Field(default_factory=list) utt_holdings: List[UTTHoldingResponse] = Field(default_factory=list) bond_holdings: List[BondHoldingResponse] = Field(default_factory=list) asset_allocation: AssetAllocation recent_transactions: List[TransactionResponse] = Field(default_factory=list) upcoming_events: List[CalendarEventResponse] = Field(default_factory=list) model_config = ConfigDict(from_attributes=True) class AssetPerformanceDetail(BaseModel): asset_id: Optional[int] = None name: str return_value: Decimal asset_type: Optional[str] = None model_config = ConfigDict(from_attributes=True) class PortfolioPerformance(BaseModel): portfolio_id: int period: str start_value: Decimal end_value: Decimal absolute_return: Decimal percentage_return: Decimal best_performer: Optional[AssetPerformanceDetail] = None worst_performer: Optional[AssetPerformanceDetail] = None model_config = ConfigDict(from_attributes=True) class PositionResponse(BaseModel): asset_id: int asset_type: str asset_name: str asset_symbol: str quantity: Decimal avg_buy_price: Decimal total_invested: Decimal current_price: Decimal current_value: Decimal profit_loss: Decimal profit_loss_percent: float model_config = ConfigDict( from_attributes=True, )