MyMidas/backend/app/schemas/investment.py
megaproxy cdc1e67321 Investment portfolio charts, search fix, and holding creation fixes
- Add four portfolio charts: allocation donut by holding, allocation
  donut by asset type, cost basis vs current value bar, return % bar
- Fix asset search to use yf.Search() full-text instead of ticker-only
  lookup — name searches like "vanguard ftse all world" now work
- Fix holding creation double-quantity bug: holdings now created with
  quantity=0 so buy transaction is sole source of quantity/cost basis
- Add per-share / total price toggle in Add Holding modal with live
  calculated equivalent shown as you type
- Add ErrorBoundary in AppShell so render errors show a message instead
  of a blank page
- Fix donut charts using || instead of ?? when falling back from
  current_value to cost_basis_total (0 was not falling through ??)
- Allow HoldingCreate.quantity >= 0 (was gt=0) to support zero-init
- Fix error display for Pydantic v2 array-of-objects validation errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:06:41 +00:00

128 lines
2.9 KiB
Python

import uuid
from datetime import date as DateType, datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, Field
InvestmentTxnType = Literal["buy", "sell", "dividend", "split", "fee", "transfer_in", "transfer_out"]
class AssetSearch(BaseModel):
id: uuid.UUID
symbol: str
name: str
type: str
currency: str
exchange: str | None
last_price: Decimal | None
price_change_24h: Decimal | None
data_source: str
model_config = {"from_attributes": True}
class AssetPricePoint(BaseModel):
date: DateType
open: Decimal | None
high: Decimal | None
low: Decimal | None
close: Decimal
volume: Decimal | None
model_config = {"from_attributes": True}
class HoldingCreate(BaseModel):
account_id: uuid.UUID
asset_id: uuid.UUID
quantity: Decimal = Field(..., ge=0)
avg_cost_basis: Decimal = Field(..., ge=0)
currency: str = Field(default="GBP", min_length=3, max_length=10)
class HoldingResponse(BaseModel):
id: uuid.UUID
account_id: uuid.UUID
asset_id: uuid.UUID
symbol: str
asset_name: str
asset_type: str
quantity: Decimal
avg_cost_basis: Decimal
current_price: Decimal | None
current_value: Decimal | None
cost_basis_total: Decimal
unrealised_gain: Decimal | None
unrealised_gain_pct: Decimal | None
currency: str
price_change_24h: Decimal | None
model_config = {"from_attributes": True}
class InvestmentTxnCreate(BaseModel):
holding_id: uuid.UUID
type: InvestmentTxnType
quantity: Decimal = Field(..., ge=0)
price: Decimal = Field(..., ge=0)
fees: Decimal = Field(default=Decimal("0"), ge=0)
currency: str = Field(default="GBP", min_length=3, max_length=10)
date: DateType
notes: str | None = None
class InvestmentTxnResponse(BaseModel):
id: uuid.UUID
holding_id: uuid.UUID
type: str
quantity: Decimal
price: Decimal
fees: Decimal
total_amount: Decimal
currency: str
date: DateType
created_at: datetime
model_config = {"from_attributes": True}
class PortfolioSummary(BaseModel):
total_value: Decimal
total_cost: Decimal
total_gain: Decimal
total_gain_pct: Decimal
currency: str
holdings: list[HoldingResponse]
class PerformanceMetrics(BaseModel):
twrr: Decimal | None
total_return: Decimal
total_return_pct: Decimal
currency: str
class CapitalGainsDisposal(BaseModel):
date: DateType
symbol: str
asset_name: str
quantity: Decimal
proceeds: Decimal
cost: Decimal
gain: Decimal
currency: str
class TaxYearSummary(BaseModel):
tax_year: str
disposals: list[CapitalGainsDisposal]
total_proceeds: Decimal
total_cost: Decimal
total_gain: Decimal
currency: str
class CapitalGainsReport(BaseModel):
tax_years: list[TaxYearSummary]
currency: str