15 KiB
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:
- Initiates market research — triggers the existing
ScannerGraphto produce a macro watchlist of top candidate tickers. - Initiates per-ticker analysis — feeds scan results into the existing
MacroBridge/TradingAgentsGraphpipeline for high-conviction candidates. - Loads current holdings — queries the Supabase database for the active portfolio state (positions, cash balance, sector weights).
- 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. - 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.
- 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.
- 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-binaryis 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_STRINGenv var (pooler URI format). - Passwords with special characters are auto-URL-encoded by
SupabaseClient._fix_dsn(). - Typical pooler URI:
postgresql://postgres.<ref>:<password>@aws-1-<region>.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:
{
"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 bridgetradingagents/report_paths.py— filesystem path conventionstradingagents/default_config.py— config pattern to followtradingagents/agents/scanners/— scanner agent examplestradingagents/graph/scanner_setup.py— parallel graph node patterns