TradingAgents/docs/portfolio/01_phase1_plan.md

11 KiB
Raw Blame History

Phase 1 Implementation Plan — Data Foundation

Goal

Build the data foundation layer for the Portfolio Manager feature.

After Phase 1 you should be able to:

  • Create and retrieve portfolios
  • Manage holdings (add, update, remove) with correct avg-cost-basis accounting
  • Record mock trades
  • Take immutable portfolio snapshots
  • Save and load all report types (scans, analysis, holding reviews, risk, PM decisions)
  • Pass a 90 %+ test coverage gate on all new modules

File Structure

tradingagents/portfolio/
├── __init__.py               ← public exports
├── models.py                 ← Portfolio, Holding, Trade, PortfolioSnapshot dataclasses
├── config.py                 ← PORTFOLIO_CONFIG dict + helpers
├── exceptions.py             ← domain exception hierarchy
├── supabase_client.py        ← Supabase CRUD wrapper
├── report_store.py           ← Filesystem document storage
├── repository.py             ← Unified data-access façade (Supabase + filesystem)
└── migrations/
    └── 001_initial_schema.sql

tests/portfolio/
├── __init__.py
├── conftest.py               ← shared fixtures
├── test_models.py
├── test_report_store.py
└── test_repository.py

Task 1 — Data Models (models.py)

Estimated effort: 23 h

Deliverables

Four dataclasses fully type-annotated:

  • Portfolio
  • Holding
  • Trade
  • PortfolioSnapshot

Each class must implement:

  • to_dict() -> dict — serialise for DB / JSON
  • from_dict(data: dict) -> Self — deserialise from DB / JSON
  • enrich(**kwargs) — attach runtime-computed fields (prices, weights, P&L)

Field Specifications

See docs/portfolio/02_data_models.md for full field tables.

Acceptance Criteria

  • All fields have explicit Python type annotations
  • to_dict()from_dict() round-trip is lossless for all fields
  • enrich() correctly computes current_value, unrealized_pnl, unrealized_pnl_pct, weight
  • 100 % line coverage in test_models.py

Task 2 — Portfolio Config (config.py)

Estimated effort: 1 h

Deliverables

PORTFOLIO_CONFIG: dict           # all tunable parameters
get_portfolio_config() -> dict   # returns merged config (defaults + env overrides)
validate_config(cfg: dict)       # raises ValueError on invalid values

Environment Variables

All are optional and default to the values shown:

Env Var Default Description
SUPABASE_CONNECTION_STRING "" PostgreSQL pooler connection URI
TRADINGAGENTS_PORTFOLIO_DATA_DIR "reports" Root dir for filesystem reports
TRADINGAGENTS_PM_MAX_POSITIONS 15 Max number of open positions
TRADINGAGENTS_PM_MAX_POSITION_PCT 0.15 Max single-position weight
TRADINGAGENTS_PM_MAX_SECTOR_PCT 0.35 Max sector weight
TRADINGAGENTS_PM_MIN_CASH_PCT 0.05 Minimum cash reserve
TRADINGAGENTS_PM_DEFAULT_BUDGET 100000.0 Default starting cash (USD)

Acceptance Criteria

  • All env vars read with os.getenv, defaulting gracefully when unset
  • validate_config raises ValueError for max_position_pct > 1.0, min_cash_pct < 0, max_positions < 1, etc.

Task 3 — Supabase Migration (migrations/001_initial_schema.sql)

Estimated effort: 12 h

Deliverables

A single idempotent SQL file (CREATE TABLE IF NOT EXISTS) that creates:

  • portfolios table
  • holdings table
  • trades table
  • snapshots table
  • All CHECK constraints
  • All FOREIGN KEY constraints
  • All indexes (PK + query-path indexes)
  • updated_at trigger function + triggers on portfolios, holdings

See docs/portfolio/03_database_schema.md for full schema.

Acceptance Criteria

  • File runs without error on a fresh Supabase PostgreSQL database
  • All tables created with correct column types and constraints
  • updated_at auto-updates on every row modification

Task 4 — Supabase Client (supabase_client.py)

Estimated effort: 34 h

Deliverables

SupabaseClient class (singleton pattern) with:

Portfolio CRUD

  • create_portfolio(portfolio: Portfolio) -> Portfolio
  • get_portfolio(portfolio_id: str) -> Portfolio
  • list_portfolios() -> list[Portfolio]
  • update_portfolio(portfolio: Portfolio) -> Portfolio
  • delete_portfolio(portfolio_id: str) -> None

Holdings CRUD

  • upsert_holding(holding: Holding) -> Holding
  • get_holding(portfolio_id: str, ticker: str) -> Holding | None
  • list_holdings(portfolio_id: str) -> list[Holding]
  • delete_holding(portfolio_id: str, ticker: str) -> None

Trades

  • record_trade(trade: Trade) -> Trade
  • list_trades(portfolio_id: str, limit: int = 100) -> list[Trade]

Snapshots

  • save_snapshot(snapshot: PortfolioSnapshot) -> PortfolioSnapshot
  • get_latest_snapshot(portfolio_id: str) -> PortfolioSnapshot | None
  • list_snapshots(portfolio_id: str, limit: int = 30) -> list[PortfolioSnapshot]

Error Handling

All methods translate Supabase/HTTP errors into domain exceptions (see Task below). Methods that query a single row raise PortfolioNotFoundError when no row is found.

Acceptance Criteria

  • Singleton — only one Supabase connection per process
  • All public methods fully type-annotated
  • Supabase integration tests auto-skip when SUPABASE_CONNECTION_STRING is unset

Task 5 — Report Store (report_store.py)

Estimated effort: 34 h

Deliverables

ReportStore class with typed save/load methods for each report type:

Method Description
save_scan(date, data) Save macro scan JSON
load_scan(date) Load macro scan JSON
save_analysis(date, ticker, data) Save per-ticker analysis report
load_analysis(date, ticker) Load per-ticker analysis report
save_holding_review(date, ticker, data) Save holding reviewer output
load_holding_review(date, ticker) Load holding reviewer output
save_risk_metrics(date, portfolio_id, data) Save risk computation output
load_risk_metrics(date, portfolio_id) Load risk computation output
save_pm_decision(date, portfolio_id, data) Save PM agent decision JSON + MD
load_pm_decision(date, portfolio_id) Load PM agent decision JSON
list_pm_decisions(portfolio_id) List all saved PM decision paths

Directory Convention

reports/daily/{date}/
├── market/
│   └── macro_scan_summary.json        ← save_scan / load_scan
├── {TICKER}/
│   └── complete_report.md             ← save_analysis / load_analysis (existing)
└── portfolio/
    ├── {TICKER}_holding_review.json   ← save_holding_review / load_holding_review
    ├── {portfolio_id}_risk_metrics.json
    ├── {portfolio_id}_pm_decision.json
    └── {portfolio_id}_pm_decision.md  (human-readable version)

Acceptance Criteria

  • Directories created automatically on first write
  • load_* returns None when the file doesn't exist (no exception)
  • JSON serialisation uses json.dumps(indent=2)

Task 6 — Repository (repository.py)

Estimated effort: 45 h

Deliverables

PortfolioRepository class — unified façade over SupabaseClient + ReportStore.

Key business logic:

add_holding(portfolio_id, ticker, shares, price):
    existing = client.get_holding(portfolio_id, ticker)
    if existing:
        new_avg_cost = (existing.avg_cost * existing.shares + price * shares)
                       / (existing.shares + shares)
        holding.shares += shares
        holding.avg_cost = new_avg_cost
    else:
        holding = Holding(ticker=ticker, shares=shares, avg_cost=price, ...)
    portfolio.cash -= shares * price   # deduct cash
    client.upsert_holding(holding)
    client.update_portfolio(portfolio)   # persist cash change

remove_holding(portfolio_id, ticker, shares, price):
    existing = client.get_holding(portfolio_id, ticker)
    if existing.shares < shares:
        raise InsufficientSharesError(...)
    if shares == existing.shares:
        client.delete_holding(portfolio_id, ticker)
    else:
        existing.shares -= shares
        client.upsert_holding(existing)
    portfolio.cash += shares * price   # credit proceeds
    client.update_portfolio(portfolio)

All DB operations execute as a logical unit (best-effort; full Supabase transactions require PG functions — deferred to Phase 3+).

Acceptance Criteria

  • add_holding correctly updates avg cost basis on repeated buys
  • remove_holding raises InsufficientSharesError when shares would go negative
  • add_holding raises InsufficientCashError when cash < shares * price
  • Repository integration tests auto-skip when SUPABASE_URL is unset

Task 7 — Package Setup

Estimated effort: 1 h

Deliverables

  1. tradingagents/portfolio/__init__.py — export public symbols
  2. pyproject.toml — add supabase>=2.0.0 to dependencies
  3. .env.example — add new env vars (SUPABASE_URL, SUPABASE_KEY, PM_*)
  4. tradingagents/default_config.py — merge PORTFOLIO_CONFIG into DEFAULT_CONFIG under a "portfolio" key (non-breaking addition)

Acceptance Criteria

  • from tradingagents.portfolio import PortfolioRepository works after install
  • pip install -e ".[dev]" succeeds with the new dependency

Task 8 — Tests

Estimated effort: 34 h

Test List

test_models.py

  • test_portfolio_to_dict_round_trip
  • test_holding_to_dict_round_trip
  • test_trade_to_dict_round_trip
  • test_snapshot_to_dict_round_trip
  • test_holding_enrich_computes_current_value
  • test_holding_enrich_computes_unrealized_pnl
  • test_holding_enrich_computes_weight
  • test_holding_enrich_handles_zero_cost

test_report_store.py

  • test_save_and_load_scan
  • test_save_and_load_analysis
  • test_save_and_load_holding_review
  • test_save_and_load_risk_metrics
  • test_save_and_load_pm_decision_json
  • test_load_returns_none_for_missing_file
  • test_list_pm_decisions
  • test_directories_created_on_write

test_repository.py (Supabase tests skip when SUPABASE_URL unset)

  • test_add_holding_new_position
  • test_add_holding_updates_avg_cost
  • test_remove_holding_full_position
  • test_remove_holding_partial_position
  • test_remove_holding_raises_insufficient_shares
  • test_add_holding_raises_insufficient_cash
  • test_record_and_list_trades
  • test_save_and_load_snapshot

Coverage Target

90 %+ for models.py and report_store.py. Integration tests (test_repository.py) auto-skip when SUPABASE_CONNECTION_STRING is unset.


Execution Order

Day 1  (parallel tracks)
  Track A: Task 1 (models) → Task 3 (SQL migration)
  Track B: Task 2 (config) → Task 7 (package setup partial)

Day 2  (parallel tracks)
  Track A: Task 4 (SupabaseClient)
  Track B: Task 5 (ReportStore)

Day 3
  Task 6 (Repository)
  Task 8 (Tests)
  Task 7 (package setup final — pyproject.toml, .env.example)

Total estimate: ~1824 hours