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:
parent
1444e8438c
commit
96a2c79cb3
|
|
@ -1,6 +1,6 @@
|
||||||
# Current Milestone
|
# 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
|
# 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
|
- Business logic: avg cost basis, cash accounting, trade recording, snapshots
|
||||||
- **PR #22 merged**: Unified report paths, structured observability logging, memory system update
|
- **PR #22 merged**: Unified report paths, structured observability logging, memory system update
|
||||||
- **feat/daily-digest-notebooklm** (shipped): Daily digest consolidation + NotebookLM source sync
|
- **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
|
# 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)
|
- Refinement of macro scan synthesis prompts (ongoing)
|
||||||
|
|
||||||
# Active Blockers
|
# Active Blockers
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,10 @@ class PortfolioGraph:
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
|
final_state = {}
|
||||||
for chunk in self.graph.stream(initial_state):
|
for chunk in self.graph.stream(initial_state):
|
||||||
print(f"[portfolio debug] chunk keys: {list(chunk.keys())}")
|
print(f"[portfolio debug] chunk keys: {list(chunk.keys())}")
|
||||||
|
final_state.update(chunk)
|
||||||
|
return final_state
|
||||||
|
|
||||||
return self.graph.invoke(initial_state)
|
return self.graph.invoke(initial_state)
|
||||||
|
|
|
||||||
|
|
@ -72,21 +72,22 @@ def score_candidate(
|
||||||
conviction_weight = _CONVICTION_WEIGHTS.get(conviction, 1.0)
|
conviction_weight = _CONVICTION_WEIGHTS.get(conviction, 1.0)
|
||||||
thesis_score = _THESIS_SCORES.get(thesis, 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)
|
max_sector_pct: float = config.get("max_sector_pct", 0.35)
|
||||||
concentration = sector_concentration(holdings, portfolio_total_value)
|
concentration = sector_concentration(holdings, portfolio_total_value)
|
||||||
current_sector_pct = concentration.get(sector, 0.0)
|
current_sector_pct = concentration.get(sector, 0.0)
|
||||||
|
|
||||||
if current_sector_pct >= max_sector_pct:
|
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:
|
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:
|
elif current_sector_pct > 0.0:
|
||||||
diversification_factor = 1.0
|
diversification_factor = 1.0 # existing sector with room
|
||||||
else:
|
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_tickers = {h.ticker for h in holdings}
|
||||||
held_penalty = 0.5 if ticker in held_tickers else 1.0
|
held_penalty = 0.5 if ticker in held_tickers else 1.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ def value_at_risk(
|
||||||
if not returns:
|
if not returns:
|
||||||
return None
|
return None
|
||||||
sorted_returns = sorted(returns)
|
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)
|
idx = max(0, int(math.floor(percentile * len(sorted_returns))) - 1)
|
||||||
return sorted_returns[idx]
|
return sorted_returns[idx]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class TradeExecutor:
|
||||||
sells = decisions.get("sells") or []
|
sells = decisions.get("sells") or []
|
||||||
buys = decisions.get("buys") or []
|
buys = decisions.get("buys") or []
|
||||||
|
|
||||||
# --- SELLs first ---
|
# --- SELLs first (frees cash before BUYs; no constraint pre-flight for sells) ---
|
||||||
for sell in sells:
|
for sell in sells:
|
||||||
ticker = (sell.get("ticker") or "").upper()
|
ticker = (sell.get("ticker") or "").upper()
|
||||||
shares = float(sell.get("shares") or 0)
|
shares = float(sell.get("shares") or 0)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue