Merge pull request #43 from aguzererler/copilot/update-portfolio-management-flow

docs: align portfolio flow with implementation, add token estimates and CLI reference
This commit is contained in:
ahmet guzererler 2026-03-21 20:50:57 +01:00 committed by GitHub
commit 01b892041b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 312 additions and 92 deletions

View File

@ -159,6 +159,62 @@ An interface will appear showing results as they load, letting you track the age
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
### CLI Commands
| Command | Description |
|---------|-------------|
| `analyze` | Interactive per-ticker multi-agent analysis (select analysts, LLM, date) |
| `scan` | Run the 3-phase macro scanner (geopolitical → sector → synthesis) |
| `pipeline` | Full pipeline: macro scan JSON → filter by conviction → per-ticker deep dive |
| `portfolio` | Run the Portfolio Manager workflow (requires portfolio ID + scan JSON) |
| `check-portfolio` | Review current holdings only — no new candidates |
| `auto` | End-to-end: scan → pipeline → portfolio manager (one command) |
**Examples:**
```bash
# Per-ticker analysis (interactive prompts for ticker, date, LLM, analysts)
python -m cli.main analyze
# Run macro scanner for a specific date
python -m cli.main scan --date 2026-03-21
# Run the full pipeline (scan → filter → per-ticker analysis)
python -m cli.main pipeline
# Run portfolio manager with a specific portfolio and scan results
python -m cli.main portfolio
# Review current holdings without new candidates
python -m cli.main check-portfolio --portfolio-id main_portfolio --date 2026-03-21
# Full autonomous mode: scan → pipeline → portfolio
python -m cli.main auto --portfolio-id main_portfolio --date 2026-03-21
```
### Running Tests
```bash
# Install dev dependencies
pip install -e ".[dev]"
# Run all unit tests (integration and e2e excluded by default)
python -m pytest tests/ -v
# Run only portfolio tests
python -m pytest tests/portfolio/ -v
# Run a specific test file
python -m pytest tests/portfolio/test_models.py -v
# Run tests with coverage (requires pytest-cov)
python -m pytest tests/ --cov=tradingagents --cov-report=term-missing
```
> **Note:** Integration tests that require network access or database connections
> auto-skip when the relevant environment variables (`SUPABASE_CONNECTION_STRING`,
> `FINNHUB_API_KEY`, etc.) are not set.
## TradingAgents Package
### Implementation Details

View File

@ -1,6 +1,6 @@
# Current Milestone
Portfolio Manager Phases 2-5 complete. All 93 tests passing (4 integration skipped).
Portfolio Manager feature fully implemented (Phases 110). All 588 tests passing (14 skipped).
# Recent Progress
@ -12,7 +12,7 @@ Portfolio Manager Phases 2-5 complete. All 93 tests passing (4 integration skipp
- Business logic: avg cost basis, cash accounting, trade recording, snapshots
- **PR #22 merged**: Unified report paths, structured observability logging, memory system update
- **feat/daily-digest-notebooklm** (shipped): Daily digest consolidation + NotebookLM source sync
- **Portfolio Manager Phases 2-5** (current branch):
- **Portfolio Manager Phases 2-5** (implemented):
- `tradingagents/portfolio/risk_evaluator.py` — pure-Python risk metrics (log returns, Sharpe, Sortino, VaR, max drawdown, beta, sector concentration, constraint checking)
- `tradingagents/portfolio/candidate_prioritizer.py` — conviction × thesis × diversification × held_penalty scoring
- `tradingagents/portfolio/trade_executor.py` — executes BUY/SELL (SELLs first), constraint pre-flight, EOD snapshot
@ -22,11 +22,13 @@ Portfolio Manager Phases 2-5 complete. All 93 tests passing (4 integration skipp
- `tradingagents/graph/portfolio_setup.py` — PortfolioGraphSetup (sequential 6-node workflow)
- `tradingagents/graph/portfolio_graph.py` — PortfolioGraph (mirrors ScannerGraph pattern)
- 48 new tests (28 risk_evaluator + 10 candidate_prioritizer + 10 trade_executor)
- **Portfolio CLI integration**: `portfolio`, `check-portfolio`, `auto` commands in `cli/main.py`
- **Documentation updated**: Flow diagram in `docs/portfolio/00_overview.md` aligned with actual 6-node sequential implementation; token estimation per model added; CLI & test commands added to README.md
# In Progress
- Portfolio Manager Phase 6: CLI integration / end-to-end wiring (next)
- Refinement of macro scan synthesis prompts (ongoing)
- End-to-end integration testing with live LLM + Supabase
# Active Blockers

View File

@ -1,6 +1,6 @@
# Portfolio Manager Agent — Design Overview
<!-- Last verified: 2026-03-20 -->
<!-- Last verified: 2026-03-21 -->
## Feature Description
@ -98,116 +98,191 @@ SQLAlchemy) and no `supabase-py` REST client is used.
---
## 5-Phase Workflow
## Implemented Workflow (6-Node Sequential Graph)
The portfolio manager runs as a **sequential LangGraph workflow** inside
`PortfolioGraph`. The scanner and price fetching happen **before** the graph
is invoked (handled by the CLI or calling code). The graph itself processes
6 nodes in strict sequence:
```
┌────────────────────────────────────────────────────────────────────────────┐
│ PHASE 1 (parallel) │
┌──────────────────────────────────┐
│ PRE-GRAPH (CLI / caller)
│ │
│ 1a. ScannerGraph.scan(date) 1b. Load Holdings + Fetch Prices │
│ → macro_scan_summary.json → List[Holding] with │
│ watchlist of top candidates current_price, current_value │
└───────────────────────────────────┬───────────────────────────────────────┘
│ • ScannerGraph.scan(date) │
│ → scan_summary JSON │
│ • yfinance price fetch │
│ → prices dict {ticker: float} │
└──────────────┬───────────────────┘
┌──────────────▼───────────────────┐
│ PortfolioGraph.run( │
│ portfolio_id, date, │
│ prices, scan_summary) │
└──────────────┬───────────────────┘
┌────────────────────────────────────▼───────────────────────────────────────┐
│ NODE 1: load_portfolio (Python, no LLM) │
│ │
│ • Queries Supabase for portfolio + holdings via PortfolioRepository │
│ • Enriches holdings with current prices and computes weights │
│ → portfolio_data (JSON string with portfolio + holdings dicts) │
└───────────────────────────────────┬────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ PHASE 2 (parallel) │
NODE 2: compute_risk (Python, no LLM)
│ │
│ 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 │
│ • Computes portfolio risk metrics from enriched holdings │
│ • Sharpe ratio (annualised, rf = 0) │
│ • Sortino ratio (downside deviation) │
│ • Portfolio beta (vs SPY)
│ • Portfolio beta (vs SPY benchmark) │
│ • 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) │
└───────────────────────────────────┬───────────────────────────────────────┘
│ → risk_metrics (JSON string) │
└───────────────────────────────────┬────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
PHASE 4 Portfolio Manager Agent (deep_think + memory)
│ NODE 3: review_holdings (LLM — mid_think) │
│ │
│ Reads: macro context, holdings, candidate signals, re-eval signals, │
│ risk metrics, budget constraint, past decisions (memory) │
│ HoldingReviewerAgent (create_holding_reviewer) │
│ • Tools: get_stock_data, get_news │
│ • Uses run_tool_loop() for inline tool execution │
│ • Reviews each open position → HOLD or SELL recommendation │
│ → holding_reviews (JSON string — ticker → review mapping) │
└───────────────────────────────────┬────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ NODE 4: prioritize_candidates (Python, no LLM) │
│ │
│ Outputs structured JSON: │
│ • Scores scan_summary.stocks_to_investigate using: │
│ conviction × thesis × diversification × held_penalty │
│ • Ranks candidates by composite score │
│ → prioritized_candidates (JSON string — sorted list) │
└───────────────────────────────────┬────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ NODE 5: pm_decision (LLM — deep_think) │
│ │
│ PM Decision Agent (create_pm_decision_agent) │
│ • Pure reasoning — no tools │
│ • Reads: portfolio_data, risk_metrics, holding_reviews, │
│ prioritized_candidates, analysis_date │
│ • Outputs structured JSON: │
│ { │
│ "sells": [{"ticker": "X", "shares": 10, "reason": "..."}], │
│ "buys": [{"ticker": "Y", "shares": 5, "reason": "..."}], │
│ "holds": ["Z"], │
│ "target_cash_pct": 0.08, │
│ "rationale": "...", │
"sells": [{"ticker": "X", "shares": 10, "rationale": "..."}],
"buys": [{"ticker": "Y", "shares": 5, "rationale": "..."}],
"holds": [{"ticker": "Z", "rationale": "..."}],
"cash_reserve_pct": 0.10, │
"portfolio_thesis": "...",
│ "risk_summary": "..." │
│ } │
└───────────────────────────────────┬───────────────────────────────────────┘
└───────────────────────────────────┬───────────────────────────────────────
┌────────────────────────────────────────────────────────────────────────────┐
│ PHASE 5 Trade Execution (Mock) │
NODE 6: execute_trades (Python, no LLM)
│ │
• 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
TradeExecutor
│ • SELLs first (frees cash), then BUYs
│ • Constraint pre-flight (position size, sector, cash)
│ • Records trades in Supabase, updates holdings & cash
│ • Takes immutable EOD portfolio snapshot │
→ execution_result (JSON string with executed/failed trades)
└────────────────────────────────────────────────────────────────────────────┘
```
### State Definition
The graph state is defined in `tradingagents/portfolio/portfolio_states.py`
as `PortfolioManagerState(MessagesState)`:
| Field | Type | Written By |
|-------|------|------------|
| `portfolio_id` | `str` | Caller (initial state) |
| `analysis_date` | `str` | Caller (initial state) |
| `prices` | `dict` | Caller (initial state) |
| `scan_summary` | `dict` | Caller (initial state) |
| `portfolio_data` | `Annotated[str, _last_value]` | Node 1 |
| `risk_metrics` | `Annotated[str, _last_value]` | Node 2 |
| `holding_reviews` | `Annotated[str, _last_value]` | Node 3 |
| `prioritized_candidates` | `Annotated[str, _last_value]` | Node 4 |
| `pm_decision` | `Annotated[str, _last_value]` | Node 5 |
| `execution_result` | `Annotated[str, _last_value]` | Node 6 |
| `sender` | `Annotated[str, _last_value]` | All nodes |
### Comparison: Original Design vs Implementation
| Aspect | Original Design (docs) | Current Implementation |
|--------|----------------------|------------------------|
| Phases 1a/1b | Parallel (scanner + load holdings) | Sequential — scanner runs in pre-graph step |
| Phases 2a/2b | Parallel (MacroBridge + HoldingReviewer) | Sequential — review_holdings then prioritize |
| Candidate analysis | MacroBridge per-ticker full pipeline | Pure Python scoring (no per-ticker LLM analysis) |
| Holding reviewer tier | `quick_think` | `mid_think` |
| Phase 3 risk metrics | Includes correlation matrix, what-if | Sharpe, Sortino, VaR, beta, drawdown, sector |
| Graph topology | Mixed parallel + sequential | Fully sequential (6 nodes) |
---
## Agent Specifications
## Agent Specifications (as implemented)
### Portfolio Manager Agent (PMA)
### Portfolio Manager Decision Agent (`pm_decision_agent.py`)
| Property | Value |
|----------|-------|
| LLM tier | `deep_think` |
| Memory | Enabled — reads previous PM decision files from filesystem |
| LLM tier | `deep_think` (default: `gpt-5.2`) |
| Pattern | `create_pm_decision_agent(llm)` → closure |
| Memory | Via context injection (portfolio_data, prior decisions) |
| Output format | Structured JSON (validated before trade execution) |
| Invocation | Once per run, after Phases 13 |
| Tools | None — pure reasoning agent |
| Invocation | Node 5 in the sequential graph, once per run |
**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)
**Prompt inputs (injected via state):**
- `portfolio_data` — Current holdings + portfolio state (JSON from Node 1)
- `risk_metrics` — Sharpe, Sortino, VaR, beta, drawdown, sector data (JSON from Node 2)
- `holding_reviews` — Per-ticker HOLD/SELL recommendations (JSON from Node 3)
- `prioritized_candidates` — Ranked candidate list with scores (JSON from Node 4)
- `analysis_date` — Date string for context
### Holding Reviewer Agent
**Output schema:**
```json
{
"sells": [{"ticker": "X", "shares": 10.0, "rationale": "..."}],
"buys": [{"ticker": "Y", "shares": 5.0, "price_target": 200.0,
"sector": "Technology", "rationale": "...", "thesis": "..."}],
"holds": [{"ticker": "Z", "rationale": "..."}],
"cash_reserve_pct": 0.10,
"portfolio_thesis": "...",
"risk_summary": "..."
}
```
### Holding Reviewer Agent (`holding_reviewer.py`)
| Property | Value |
|----------|-------|
| LLM tier | `quick_think` |
| LLM tier | `mid_think` (default: falls back to `gpt-5-mini`) |
| Pattern | `create_holding_reviewer(llm)` → closure |
| 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) |
| Tools | `get_stock_data`, `get_news` |
| Tool execution | `run_tool_loop()` inline (up to 5 rounds) |
| Invocation | Node 3 in the sequential graph, once per run |
**Output schema per holding:**
```json
{
"AAPL": {
"ticker": "AAPL",
"signal": "HOLD",
"confidence": 0.72,
"reason": "Price action neutral; no material news. RSI 52, MACD flat.",
"urgency": "LOW"
"recommendation": "HOLD",
"confidence": "high",
"rationale": "Strong momentum, no negative news.",
"key_risks": ["Sector concentration", "Valuation stretch"]
}
}
```
@ -241,29 +316,116 @@ These rules trigger specific actions and are part of the PM Agent's system promp
---
## 10-Phase Implementation Roadmap
## Implementation Roadmap (Status)
| Phase | Deliverable | Effort |
|-------|-------------|--------|
| 1 | Data foundation (this PR) — models, DB, filesystem, repository | ~23 days |
| 2 | Holding Reviewer Agent | ~1 day |
| 3 | Risk metrics engine (Phase 3 of workflow) | ~12 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 |
| Phase | Deliverable | Status | Source |
|-------|-------------|--------|--------|
| 1 | Data foundation — models, DB, filesystem, repository | ✅ Done (PR #32) | `tradingagents/portfolio/models.py`, `repository.py`, etc. |
| 2 | Holding Reviewer Agent | ✅ Done | `tradingagents/agents/portfolio/holding_reviewer.py` |
| 3 | Risk metrics engine | ✅ Done | `tradingagents/portfolio/risk_evaluator.py`, `risk_metrics.py` |
| 4 | Portfolio Manager Decision Agent (LLM, structured output) | ✅ Done | `tradingagents/agents/portfolio/pm_decision_agent.py` |
| 5 | Trade execution engine | ✅ Done | `tradingagents/portfolio/trade_executor.py` |
| 6 | Full orchestration graph (LangGraph) | ✅ Done | `tradingagents/graph/portfolio_graph.py`, `portfolio_setup.py` |
| 7 | CLI commands (`portfolio`, `check-portfolio`, `auto`) | ✅ Done | `cli/main.py` |
| 8 | Candidate prioritizer | ✅ Done | `tradingagents/portfolio/candidate_prioritizer.py` |
| 9 | Portfolio state for LangGraph | ✅ Done | `tradingagents/portfolio/portfolio_states.py` |
| 10 | Tests (models, report_store, risk, trade, candidates) | ✅ Done | `tests/portfolio/` (588 tests total, 14 skipped) |
**Total estimate: ~1522 days**
---
## Token Estimation per Model
Estimated token usage per model tier across all three workflows. Numbers
assume default models (`quick_think` = gpt-5-mini, `deep_think` = gpt-5.2,
`mid_think` falls back to gpt-5-mini when not configured).
### Trading Workflow (`TradingAgentsGraph.propagate`)
| Agent | LLM Tier | Tools | LLM Calls | Est. Input Tokens | Est. Output Tokens |
|-------|----------|-------|-----------|-------------------|-------------------|
| Market Analyst | quick_think | get_stock_data, get_indicators, get_macro_regime | 35 | ~3,0005,000 | ~1,5003,000 |
| Social Media Analyst | quick_think | get_news | 12 | ~2,0003,000 | ~1,0002,000 |
| News Analyst | quick_think | get_news, get_global_news | 23 | ~2,0004,000 | ~1,0002,500 |
| Fundamentals Analyst | quick_think | get_ttm_analysis, get_fundamentals, etc. | 46 | ~4,0008,000 | ~2,0004,000 |
| Bull Researcher | mid_think | — | 12 per round | ~4,0008,000 | ~1,5003,000 |
| Bear Researcher | mid_think | — | 12 per round | ~4,0008,000 | ~1,5003,000 |
| Research Manager (Judge) | deep_think | — | 1 | ~6,00012,000 | ~2,0004,000 |
| Trader | mid_think | — | 1 | ~3,0005,000 | ~1,0002,000 |
| Aggressive Risk Analyst | quick_think | — | 12 per round | ~3,0006,000 | ~1,0002,000 |
| Neutral Risk Analyst | quick_think | — | 12 per round | ~3,0006,000 | ~1,0002,000 |
| Conservative Risk Analyst | quick_think | — | 12 per round | ~3,0006,000 | ~1,0002,000 |
| Risk Judge | deep_think | — | 1 | ~6,00012,000 | ~2,0004,000 |
**Trading workflow totals** (with `max_debate_rounds=2`):
- **LLM calls**: ~1927
- **Tool calls**: ~1525
- **quick_think tokens**: ~35,00055,000 input, ~12,00020,000 output
- **deep_think tokens**: ~12,00024,000 input, ~4,0008,000 output
### Scanner Workflow (`ScannerGraph.scan`)
| Agent | LLM Tier | Tools | LLM Calls | Est. Input Tokens | Est. Output Tokens |
|-------|----------|-------|-----------|-------------------|-------------------|
| Geopolitical Scanner | quick_think | get_topic_news | 23 | ~2,0004,000 | ~1,0002,500 |
| Market Movers Scanner | quick_think | get_market_movers, get_market_indices | 23 | ~2,0004,000 | ~1,0002,500 |
| Sector Scanner | quick_think | get_sector_performance | 12 | ~1,5003,000 | ~8002,000 |
| Industry Deep Dive | mid_think | get_industry_performance, get_topic_news | 57 | ~6,00010,000 | ~3,0005,000 |
| Macro Synthesis | deep_think | — | 1 | ~8,00015,000 | ~3,0005,000 |
**Scanner workflow totals**:
- **LLM calls**: ~913
- **Tool calls**: ~1116
- **quick_think tokens**: ~5,50011,000 input, ~2,8007,000 output
- **deep_think tokens**: ~8,00015,000 input, ~3,0005,000 output
### Portfolio Workflow (`PortfolioGraph.run`)
| Node | LLM Tier | Tools | LLM Calls | Est. Input Tokens | Est. Output Tokens |
|------|----------|-------|-----------|-------------------|-------------------|
| load_portfolio | — | — | 0 | 0 | 0 |
| compute_risk | — | — | 0 | 0 | 0 |
| review_holdings | mid_think | get_stock_data, get_news | 1 call reviews all holdings (up to 5 tool rounds) | ~3,0006,000 | ~1,5003,000 |
| prioritize_candidates | — | — | 0 | 0 | 0 |
| pm_decision | deep_think | — | 1 | ~6,00012,000 | ~2,0004,000 |
| execute_trades | — | — | 0 | 0 | 0 |
**Portfolio workflow totals** (assuming 5 holdings):
- **LLM calls**: 2 (review + decision)
- **Tool calls**: ~10 (2 per holding)
- **mid_think tokens**: ~3,0006,000 input, ~1,5003,000 output
- **deep_think tokens**: ~6,00012,000 input, ~2,0004,000 output
### Full Auto Mode (`scan → pipeline → portfolio`)
| Workflow | quick_think | deep_think |
|----------|-------------|------------|
| Scanner | ~5K11K in / ~3K7K out | ~8K15K in / ~3K5K out |
| Trading (× 3 tickers — aggregate) | ~105K165K in / ~36K60K out | ~36K72K in / ~12K24K out |
| Portfolio | — | ~6K12K in / ~2K4K out |
| **Totals** | **~110K176K in / ~39K67K out** | **~50K99K in / ~17K33K out** |
> **Note:** `mid_think` defaults to `quick_think_llm` when not configured,
> so mid_think token counts are included under quick_think totals above.
> Actual token usage varies with portfolio size, number of candidates, and
> debate rounds.
---
## 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
- `tradingagents/graph/portfolio_graph.py` — PortfolioGraph orchestrator
- `tradingagents/graph/portfolio_setup.py` — 6-node sequential workflow setup
- `tradingagents/portfolio/portfolio_states.py` — PortfolioManagerState definition
- `tradingagents/agents/portfolio/holding_reviewer.py` — Holding reviewer LLM agent
- `tradingagents/agents/portfolio/pm_decision_agent.py` — PM decision LLM agent
- `tradingagents/portfolio/risk_evaluator.py` — Pure-Python risk metrics
- `tradingagents/portfolio/candidate_prioritizer.py` — Candidate scoring/ranking
- `tradingagents/portfolio/trade_executor.py` — Trade execution with constraint checks
- `tradingagents/portfolio/models.py` — Portfolio, Holding, Trade, PortfolioSnapshot
- `tradingagents/portfolio/repository.py` — Unified data-access façade
- `tradingagents/portfolio/report_store.py` — Filesystem document storage
- `tradingagents/pipeline/macro_bridge.py` — Existing scan → per-ticker bridge
- `tradingagents/report_paths.py` — Filesystem path conventions
- `tradingagents/default_config.py` — Config pattern and LLM tier defaults
- `tradingagents/graph/scanner_graph.py` — Scanner pipeline (runs before portfolio)
- `cli/main.py` — CLI commands: `portfolio`, `check-portfolio`, `auto`