# Data Models — Full Specification All models live in `tradingagents/portfolio/models.py` as Python `dataclass` types. They must be fully type-annotated and support lossless `to_dict` / `from_dict` round-trips. --- ## `Portfolio` Represents a single managed portfolio (one user may eventually have multiple). ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `portfolio_id` | `str` | Yes | UUID, primary key | | `name` | `str` | Yes | Human-readable name, e.g. "Main Portfolio" | | `cash` | `float` | Yes | Available cash balance in USD | | `initial_cash` | `float` | Yes | Starting capital (immutable after creation) | | `currency` | `str` | Yes | ISO 4217 code, default `"USD"` | | `created_at` | `str` | Yes | ISO-8601 UTC datetime string | | `updated_at` | `str` | Yes | ISO-8601 UTC datetime string | | `report_path` | `str \| None` | No | Filesystem path to today's portfolio report dir | | `metadata` | `dict` | No | Free-form JSON for agent notes / tags | ### Computed / Derived Fields (not stored in DB) | Field | Type | Description | |-------|------|-------------| | `total_value` | `float` | `cash` + sum of all holding `current_value` | | `equity_value` | `float` | sum of all holding `current_value` | | `cash_pct` | `float` | `cash / total_value` | ### Methods ```python def to_dict(self) -> dict: """Serialise all stored fields to a flat dict suitable for JSON / Supabase insert.""" def from_dict(cls, data: dict) -> "Portfolio": """Deserialise from a DB row or JSON dict. Missing optional fields default gracefully.""" def enrich(self, holdings: list["Holding"]) -> "Portfolio": """Compute total_value, equity_value, cash_pct from the provided holdings list.""" ``` --- ## `Holding` Represents a single open position within a portfolio. ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `holding_id` | `str` | Yes | UUID, primary key | | `portfolio_id` | `str` | Yes | FK → portfolios.portfolio_id | | `ticker` | `str` | Yes | Stock ticker symbol, e.g. `"AAPL"` | | `shares` | `float` | Yes | Number of shares held | | `avg_cost` | `float` | Yes | Average cost basis per share (USD) | | `sector` | `str \| None` | No | GICS sector name | | `industry` | `str \| None` | No | GICS industry name | | `created_at` | `str` | Yes | ISO-8601 UTC datetime string | | `updated_at` | `str` | Yes | ISO-8601 UTC datetime string | ### Runtime-Computed Fields (not stored in DB) These are populated by `enrich()` and available for agent/analysis use: | Field | Type | Description | |-------|------|-------------| | `current_price` | `float \| None` | Latest market price per share | | `current_value` | `float \| None` | `current_price * shares` | | `cost_basis` | `float` | `avg_cost * shares` | | `unrealized_pnl` | `float \| None` | `current_value - cost_basis` | | `unrealized_pnl_pct` | `float \| None` | `unrealized_pnl / cost_basis` (0 if cost_basis == 0) | | `weight` | `float \| None` | `current_value / portfolio_total_value` | ### Methods ```python def to_dict(self) -> dict: """Serialise stored fields only (not runtime-computed fields).""" def from_dict(cls, data: dict) -> "Holding": """Deserialise from DB row or JSON dict.""" def enrich(self, current_price: float, portfolio_total_value: float) -> "Holding": """ Populate runtime-computed fields in-place and return self. Args: current_price: Latest market price for this ticker. portfolio_total_value: Total portfolio value (cash + equity) for weight calc. """ ``` --- ## `Trade` Immutable record of a single mock trade execution. Never modified after creation. ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `trade_id` | `str` | Yes | UUID, primary key | | `portfolio_id` | `str` | Yes | FK → portfolios.portfolio_id | | `ticker` | `str` | Yes | Stock ticker symbol | | `action` | `str` | Yes | `"BUY"` or `"SELL"` | | `shares` | `float` | Yes | Number of shares traded | | `price` | `float` | Yes | Execution price per share (USD) | | `total_value` | `float` | Yes | `shares * price` | | `trade_date` | `str` | Yes | ISO-8601 UTC datetime of execution | | `rationale` | `str \| None` | No | PM agent rationale for this trade | | `signal_source` | `str \| None` | No | `"scanner"`, `"holding_review"`, `"pm_agent"` | | `metadata` | `dict` | No | Free-form JSON | ### Methods ```python def to_dict(self) -> dict: """Serialise all fields.""" def from_dict(cls, data: dict) -> "Trade": """Deserialise from DB row or JSON dict.""" ``` --- ## `PortfolioSnapshot` Point-in-time immutable record of the portfolio state. Taken after every trade execution session (Phase 5 of the workflow). Used for performance tracking. ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `snapshot_id` | `str` | Yes | UUID, primary key | | `portfolio_id` | `str` | Yes | FK → portfolios.portfolio_id | | `snapshot_date` | `str` | Yes | ISO-8601 UTC datetime | | `total_value` | `float` | Yes | Cash + equity at snapshot time | | `cash` | `float` | Yes | Cash balance at snapshot time | | `equity_value` | `float` | Yes | Sum of position values at snapshot time | | `num_positions` | `int` | Yes | Number of open positions | | `holdings_snapshot` | `list[dict]` | Yes | Serialised list of holding dicts (as-of) | | `metadata` | `dict` | No | Free-form JSON (e.g. PM decision path) | ### Methods ```python def to_dict(self) -> dict: """Serialise all fields. `holdings_snapshot` is already a list[dict].""" def from_dict(cls, data: dict) -> "PortfolioSnapshot": """Deserialise. `holdings_snapshot` parsed from JSON string if needed.""" ``` --- ## Serialisation Contract ### `to_dict()` - Returns a flat `dict[str, Any]` - All values must be JSON-serialisable (str, int, float, bool, list, dict, None) - `datetime` objects → ISO-8601 string (`isoformat()`) - `Decimal` values → `float` - Runtime-computed fields (`current_price`, `weight`, etc.) are **excluded** - Complex nested fields (`metadata`, `holdings_snapshot`) are included as-is ### `from_dict()` - Class method; must be callable as `Portfolio.from_dict(row)` - Handles missing optional fields with `data.get("field", default)` - Does **not** raise on extra keys in `data` - Does **not** populate runtime-computed fields (call `enrich()` separately) --- ## Enrichment Logic ### `Holding.enrich(current_price, portfolio_total_value)` ```python self.current_price = current_price self.current_value = current_price * self.shares self.cost_basis = self.avg_cost * self.shares self.unrealized_pnl = self.current_value - self.cost_basis if self.cost_basis > 0: self.unrealized_pnl_pct = self.unrealized_pnl / self.cost_basis else: self.unrealized_pnl_pct = 0.0 if portfolio_total_value > 0: self.weight = self.current_value / portfolio_total_value else: self.weight = 0.0 return self ``` ### `Portfolio.enrich(holdings)` ```python self.equity_value = sum(h.current_value or 0 for h in holdings) self.total_value = self.cash + self.equity_value if self.total_value > 0: self.cash_pct = self.cash / self.total_value else: self.cash_pct = 1.0 return self ``` --- ## Type Alias Reference ```python from __future__ import annotations from dataclasses import dataclass, field from typing import Any ``` All `metadata` fields use `dict[str, Any]` with `field(default_factory=dict)`. All optional fields default to `None` unless noted otherwise.