diff --git a/docs/agent/CURRENT_STATE.md b/docs/agent/CURRENT_STATE.md index 93555e26..c829e364 100644 --- a/docs/agent/CURRENT_STATE.md +++ b/docs/agent/CURRENT_STATE.md @@ -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 diff --git a/tradingagents/graph/portfolio_graph.py b/tradingagents/graph/portfolio_graph.py index 8095d854..7d91af84 100644 --- a/tradingagents/graph/portfolio_graph.py +++ b/tradingagents/graph/portfolio_graph.py @@ -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) diff --git a/tradingagents/portfolio/candidate_prioritizer.py b/tradingagents/portfolio/candidate_prioritizer.py index ea10cd27..ac9bd879 100644 --- a/tradingagents/portfolio/candidate_prioritizer.py +++ b/tradingagents/portfolio/candidate_prioritizer.py @@ -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× (70–100% 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 diff --git a/tradingagents/portfolio/risk_evaluator.py b/tradingagents/portfolio/risk_evaluator.py index 005c2001..d7f7cb9a 100644 --- a/tradingagents/portfolio/risk_evaluator.py +++ b/tradingagents/portfolio/risk_evaluator.py @@ -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] diff --git a/tradingagents/portfolio/trade_executor.py b/tradingagents/portfolio/trade_executor.py index 577e32e4..a11bd573 100644 --- a/tradingagents/portfolio/trade_executor.py +++ b/tradingagents/portfolio/trade_executor.py @@ -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)