Implement Portfolio Manager Phases 2-5: risk evaluation, candidate prioritization, holding reviewer agent, PM decision agent, trade executor, and portfolio graph orchestration

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-20 14:44:22 +00:00
parent 1444e8438c
commit 96a2c79cb3
5 changed files with 27 additions and 9 deletions

View File

@ -1,6 +1,6 @@
# Current Milestone
Portfolio Manager Phase 1 (data foundation) complete and merged. All 4 Supabase tables live, 51 tests passing (including integration tests against live DB).
Portfolio Manager Phases 2-5 complete. All 93 tests passing (4 integration skipped).
# Recent Progress
@ -12,10 +12,20 @@ Portfolio Manager Phase 1 (data foundation) complete and merged. All 4 Supabase
- 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):
- `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
- `tradingagents/agents/portfolio/holding_reviewer.py` — LLM holding review agent (run_tool_loop pattern)
- `tradingagents/agents/portfolio/pm_decision_agent.py` — pure-reasoning PM decision agent (no tools)
- `tradingagents/portfolio/portfolio_states.py` — PortfolioManagerState (MessagesState + reducers)
- `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)
# In Progress
- Portfolio Manager Phase 2: Holding Reviewer Agent (next)
- Portfolio Manager Phase 6: CLI integration / end-to-end wiring (next)
- Refinement of macro scan synthesis prompts (ongoing)
# Active Blockers

View File

@ -168,7 +168,10 @@ class PortfolioGraph:
}
if self.debug:
final_state = {}
for chunk in self.graph.stream(initial_state):
print(f"[portfolio debug] chunk keys: {list(chunk.keys())}")
final_state.update(chunk)
return final_state
return self.graph.invoke(initial_state)

View File

@ -72,21 +72,22 @@ def score_candidate(
conviction_weight = _CONVICTION_WEIGHTS.get(conviction, 1.0)
thesis_score = _THESIS_SCORES.get(thesis, 1.0)
# Diversification factor based on sector exposure
# Diversification factor based on sector exposure.
# Tiered: 0.0× (sector full), 0.5× (70100% of limit), 1.0× (under 70%), 2.0× (new sector).
max_sector_pct: float = config.get("max_sector_pct", 0.35)
concentration = sector_concentration(holdings, portfolio_total_value)
current_sector_pct = concentration.get(sector, 0.0)
if current_sector_pct >= max_sector_pct:
diversification_factor = 0.0
diversification_factor = 0.0 # sector at or above limit — skip
elif current_sector_pct >= 0.70 * max_sector_pct:
diversification_factor = 0.5
diversification_factor = 0.5 # near limit — reduced bonus
elif current_sector_pct > 0.0:
diversification_factor = 1.0
diversification_factor = 1.0 # existing sector with room
else:
diversification_factor = 2.0
diversification_factor = 2.0 # new sector — diversification bonus
# Held penalty
# Held penalty: already-owned tickers score half (exposure already taken).
held_tickers = {h.ticker for h in holdings}
held_penalty = 0.5 if ticker in held_tickers else 1.0

View File

@ -108,6 +108,10 @@ def value_at_risk(
if not returns:
return None
sorted_returns = sorted(returns)
# Require at least 20 observations for a statistically meaningful VaR estimate.
# With fewer points the percentile calculation is unreliable.
if len(sorted_returns) < 20:
return None
idx = max(0, int(math.floor(percentile * len(sorted_returns))) - 1)
return sorted_returns[idx]

View File

@ -77,7 +77,7 @@ class TradeExecutor:
sells = decisions.get("sells") or []
buys = decisions.get("buys") or []
# --- SELLs first ---
# --- SELLs first (frees cash before BUYs; no constraint pre-flight for sells) ---
for sell in sells:
ticker = (sell.get("ticker") or "").upper()
shares = float(sell.get("shares") or 0)