Mbonea's picture
initial commit
9d4bd7c
# 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,
)