MyMidas/backend/app/schemas/investment.py
megaproxy fe4e69b9ad Complete Phase 3, Phase 5 polish and hardening
Phase 3 — Investments:
- Multi-currency support: holdings track purchase currency, FX rates convert to base for totals
- Capital gains report using UK Section 104 pool method, grouped by tax year
- Capital Gains tab added to Reports page

Phase 5 — Polish & Hardening:
- Mobile-responsive layout: bottom nav, sidebar hidden on mobile, logo in TopBar, compact header buttons, hover-only actions now always visible on touch
- Backup system: encrypted GPG backups via backup.sh, nightly scheduler job, admin API (list/trigger/download/restore), Settings UI with drag-to-restore confirmation
- Docker entrypoint with gosu privilege drop to fix bind-mount ownership on fresh deployments
- OWASP fixes: refresh token now bound to its session (new refresh_token_hash column + migration), CSRF secure flag tied to environment, IP-level rate limiting on login, TOTPEnableRequest Pydantic schema replaces raw dict
- AES-256-GCM key rotation script (rotate_keys.py) with dry-run mode and atomic DB transaction
- CLAUDE.md added for AI-assisted development context
- README updated: correct reverse proxy port, accurate backup/restore commands, key rotation instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 14:59:11 +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(..., gt=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