# Portfolio Manager Agent — Design Overview ## Feature Description The Portfolio Manager Agent (PMA) is an autonomous agent that manages a simulated investment portfolio end-to-end. It performs the following actions in sequence: 1. **Initiates market research** — triggers the existing `ScannerGraph` to produce a macro watchlist of top candidate tickers. 2. **Initiates per-ticker analysis** — feeds scan results into the existing `MacroBridge` / `TradingAgentsGraph` pipeline for high-conviction candidates. 3. **Loads current holdings** — queries the Supabase database for the active portfolio state (positions, cash balance, sector weights). 4. **Requests lightweight holding reviews** — for each existing holding, runs a quick `HoldingReviewerAgent` (quick_think) that checks price action and recent news — no full bull/bear debate needed. 5. **Computes portfolio-level risk metrics** — pure Python, no LLM: Sharpe ratio, Sortino ratio, beta, 95 % VaR, max drawdown, sector concentration, correlation matrix, and what-if buy/sell scenarios. 6. **Makes allocation decisions** — the Portfolio Manager Agent (deep_think + memory) reads all inputs and outputs a structured JSON with sells, buys, holds, target cash %, and detailed rationale. 7. **Executes mock trades** — validates decisions against constraints, records trades in Supabase, updates holdings, and takes an immutable snapshot. --- ## Architecture Decision: Supabase (PostgreSQL) + Filesystem ``` ┌─────────────────────────────────────────────────┐ │ Supabase (PostgreSQL) │ │ │ │ portfolios holdings trades snapshots │ │ │ │ "What do I own right now?" │ │ "What trades did I make?" │ │ "What was my portfolio value on date X?" │ └────────────────────┬────────────────────────────┘ │ report_path column ▼ ┌─────────────────────────────────────────────────┐ │ Filesystem (reports/) │ │ │ │ reports/daily/{date}/ │ │ market/ ← scan output │ │ {TICKER}/ ← per-ticker analysis │ │ portfolio/ │ │ holdings_review.json │ │ risk_metrics.json │ │ pm_decision.json │ │ pm_decision.md (human-readable) │ │ │ │ "Why did I decide this?" │ │ "What was the macro context?" │ │ "What did the risk model say?" │ └─────────────────────────────────────────────────┘ ``` **Rationale:** | Concern | Storage | Why | |---------|---------|-----| | Transactional integrity (trades) | Supabase | ACID, foreign keys, row-level security | | Fast portfolio queries (weights, cash) | Supabase | SQL aggregations | | LLM reports (large text, markdown) | Filesystem | Avoids bloating the DB | | Agent memory / rationale | Filesystem | Easy to inspect and version | | Audit trail of decisions | Filesystem | Markdown readable by humans | The `report_path` column in the `portfolios` table points to the daily portfolio subdirectory on disk: `reports/daily/{date}/portfolio/`. ### Data Access Layer: raw `psycopg2` (no ORM) The Python code talks to Supabase PostgreSQL directly via `psycopg2` using the **pooler connection string** (`SUPABASE_CONNECTION_STRING`). No ORM (Prisma, SQLAlchemy) and no `supabase-py` REST client is used. **Why `psycopg2` over `supabase-py`?** - Direct SQL gives full control — transactions, upserts, `RETURNING *`, CTEs. - No dependency on Supabase's PostgREST schema cache or API key types. - `psycopg2-binary` is a single pip install with zero non-Python dependencies. - 4 tables with straightforward CRUD don't benefit from an ORM or REST wrapper. **Connection:** - Uses `SUPABASE_CONNECTION_STRING` env var (pooler URI format). - Passwords with special characters are auto-URL-encoded by `SupabaseClient._fix_dsn()`. - Typical pooler URI: `postgresql://postgres.:@aws-1-.pooler.supabase.com:6543/postgres` **Why not Prisma / SQLAlchemy?** - Prisma requires Node.js runtime — extra non-Python dependency. - SQLAlchemy adds dependency overhead for 4 simple tables. - Plain SQL migration files are readable, versionable, and Supabase-native. > Full rationale: `docs/agent/decisions/012-portfolio-no-orm.md` --- ## 5-Phase Workflow ``` ┌────────────────────────────────────────────────────────────────────────────┐ │ PHASE 1 (parallel) │ │ │ │ 1a. ScannerGraph.scan(date) 1b. Load Holdings + Fetch Prices │ │ → macro_scan_summary.json → List[Holding] with │ │ watchlist of top candidates current_price, current_value │ └───────────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────────────────┐ │ PHASE 2 (parallel) │ │ │ │ 2a. New Candidate Analysis 2b. Holding Re-evaluation │ │ MacroBridge.run_all_tickers() HoldingReviewerAgent (quick_think)│ │ Full bull/bear pipeline per 7-day price + 3-day news │ │ HIGH/MEDIUM conviction → JSON: signal/confidence/reason │ │ candidates that are NOT urgency per holding │ │ already held │ └───────────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────────────────┐ │ PHASE 3 (Python, no LLM) │ │ │ │ Risk Metrics Computation │ │ • Sharpe ratio (annualised, rf = 0) │ │ • Sortino ratio (downside deviation) │ │ • Portfolio beta (vs SPY) │ │ • 95 % VaR (historical simulation, 30-day window) │ │ • Max drawdown (peak-to-trough, 90-day window) │ │ • Sector concentration (weight per GICS sector) │ │ • Correlation matrix (all holdings) │ │ • What-if scenarios (buy X, sell Y → new weights) │ └───────────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────────────────┐ │ PHASE 4 Portfolio Manager Agent (deep_think + memory) │ │ │ │ Reads: macro context, holdings, candidate signals, re-eval signals, │ │ risk metrics, budget constraint, past decisions (memory) │ │ │ │ Outputs structured JSON: │ │ { │ │ "sells": [{"ticker": "X", "shares": 10, "reason": "..."}], │ │ "buys": [{"ticker": "Y", "shares": 5, "reason": "..."}], │ │ "holds": ["Z"], │ │ "target_cash_pct": 0.08, │ │ "rationale": "...", │ │ "risk_summary": "..." │ │ } │ └───────────────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────────────────┐ │ PHASE 5 Trade Execution (Mock) │ │ │ │ • Validate decisions against constraints (position size, sector, cash) │ │ • Record each trade in Supabase (trades table) │ │ • Update holdings (avg cost basis, shares) │ │ • Deduct / credit cash balance │ │ • Take immutable portfolio snapshot │ │ • Save PM decision + risk report to filesystem │ └────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Agent Specifications ### Portfolio Manager Agent (PMA) | Property | Value | |----------|-------| | LLM tier | `deep_think` | | Memory | Enabled — reads previous PM decision files from filesystem | | Output format | Structured JSON (validated before trade execution) | | Invocation | Once per run, after Phases 1–3 | **Prompt inputs:** - Macro scan summary (top candidates + context) - Current holdings list (ticker, shares, avg cost, current price, weight, sector) - Candidate analysis signals (BUY/SELL/HOLD per ticker from Phase 2a) - Holding review signals (signal, confidence, reason, urgency per holding from Phase 2b) - Risk metrics report (Phase 3 output) - Budget constraint (available cash) - Portfolio constraints (see below) - Previous decision (last PM decision file for memory continuity) ### Holding Reviewer Agent | Property | Value | |----------|-------| | LLM tier | `quick_think` | | Memory | Disabled | | Output format | Structured JSON | | Tools | `get_stock_data` (7-day window), `get_news` (3-day window), RSI, MACD | | Invocation | Once per existing holding (parallelisable) | **Output schema per holding:** ```json { "ticker": "AAPL", "signal": "HOLD", "confidence": 0.72, "reason": "Price action neutral; no material news. RSI 52, MACD flat.", "urgency": "LOW" } ``` --- ## PM Agent Constraints These constraints are **hard limits** enforced during Phase 5 (trade execution). The PM Agent is also instructed to respect them in its prompt. | Constraint | Value | |------------|-------| | Max position size | 15 % of portfolio value | | Max sector exposure | 35 % of portfolio value | | Min cash reserve | 5 % of portfolio value | | Max number of positions | 15 | --- ## PM Risk Management Rules These rules trigger specific actions and are part of the PM Agent's system prompt: | Trigger | Action | |---------|--------| | Portfolio beta > 1.3 | Reduce cyclical / high-beta positions | | Sector exposure > 35 % | Diversify — sell smallest position in that sector | | Sharpe ratio < 0.5 | Raise cash — reduce overall exposure | | Max drawdown > 15 % | Go defensive — reduce equity allocation | | Daily 95 % VaR > 3 % | Reduce position sizes to lower tail risk | --- ## 10-Phase Implementation Roadmap | Phase | Deliverable | Effort | |-------|-------------|--------| | 1 | Data foundation (this PR) — models, DB, filesystem, repository | ~2–3 days | | 2 | Holding Reviewer Agent | ~1 day | | 3 | Risk metrics engine (Phase 3 of workflow) | ~1–2 days | | 4 | Portfolio Manager Agent (LLM, structured output) | ~2 days | | 5 | Trade execution engine (Phase 5 of workflow) | ~1 day | | 6 | Full orchestration graph (LangGraph) tying all phases | ~2 days | | 7 | CLI command `pm run` | ~0.5 days | | 8 | End-to-end integration tests | ~1 day | | 9 | Performance tuning + concurrency (Phase 2 parallelism) | ~1 day | | 10 | Documentation, memory system update, PR review | ~0.5 days | **Total estimate: ~15–22 days** --- ## References - `tradingagents/pipeline/macro_bridge.py` — existing scan → per-ticker bridge - `tradingagents/report_paths.py` — filesystem path conventions - `tradingagents/default_config.py` — config pattern to follow - `tradingagents/agents/scanners/` — scanner agent examples - `tradingagents/graph/scanner_setup.py` — parallel graph node patterns