620 lines
27 KiB
Python
620 lines
27 KiB
Python
"""Advanced portfolio orchestrator that prioritizes hypotheses and schedules analysts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from datetime import date, datetime, timedelta
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
from tradingagents.dataflows.interface import route_to_vendor
|
|
|
|
|
|
def create_portfolio_orchestrator(
|
|
llm: Any,
|
|
profile: Dict[str, object],
|
|
context_fetcher: Callable[[List[str]], List[Dict[str, str]]],
|
|
fast_news_fetcher: Callable[[str, str, int, int], Dict[str, str]],
|
|
plan_generator: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
|
|
):
|
|
"""Create an orchestrator node that generates and manages portfolio hypotheses."""
|
|
|
|
profile_name = profile.get("profile_name", "Portfolio")
|
|
mandate = profile.get("mandate", "")
|
|
risk_limits = profile.get("risk_limits", {})
|
|
notes = profile.get("notes", "")
|
|
universe_raw = profile.get("universe", "")
|
|
universe = [symbol.strip().upper() for symbol in universe_raw.split(",") if symbol.strip()]
|
|
sentiment_lookback = profile.get("sentiment_lookback_days", 2)
|
|
headline_limit = profile.get("news_headline_limit", 5)
|
|
market_lookback = profile.get("market_data_lookback_days", 30)
|
|
trade_policy = profile.get("trade_activation", {})
|
|
threshold = profile.get("hypothesis_threshold", 0.6)
|
|
max_hypotheses = profile.get("max_concurrent_hypotheses", 2)
|
|
|
|
risk_blurb = (
|
|
f"Max single position: {risk_limits.get('max_single_position_pct', 'n/a')} | "
|
|
f"Max sector: {risk_limits.get('max_sector_pct', 'n/a')}"
|
|
)
|
|
|
|
def _parse_account(account_text: str) -> Dict[str, str]:
|
|
summary = {}
|
|
pattern = re.compile(r"^([A-Za-z ]+):\s*\$?([\d\.,\-]+)")
|
|
for line in account_text.splitlines():
|
|
match = pattern.match(line.strip())
|
|
if match:
|
|
key = match.group(1).strip().lower().replace(" ", "_")
|
|
summary[key] = match.group(2).strip()
|
|
return summary
|
|
|
|
def _parse_positions(positions_text: str) -> List[Dict[str, str]]:
|
|
positions: List[Dict[str, str]] = []
|
|
current: Dict[str, str] = {}
|
|
for line in positions_text.splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
if line.startswith("Symbol:"):
|
|
if current:
|
|
positions.append(current)
|
|
current = {}
|
|
if ":" in line:
|
|
key, value = line.split(":", 1)
|
|
current[key.strip().lower().replace(" ", "_")] = value.strip()
|
|
if current:
|
|
positions.append(current)
|
|
return positions
|
|
|
|
def _to_float(value: Any) -> float:
|
|
if value is None:
|
|
return 0.0
|
|
if isinstance(value, (int, float)):
|
|
return float(value)
|
|
text = str(value).strip()
|
|
if not text:
|
|
return 0.0
|
|
cleaned = text.replace("$", "").replace(",", "")
|
|
try:
|
|
return float(cleaned)
|
|
except ValueError:
|
|
return 0.0
|
|
|
|
def orchestrator_node(state):
|
|
override_symbols_raw = state.get("orchestrator_focus_override") or []
|
|
override_symbols = [str(sym).upper() for sym in override_symbols_raw if str(sym).strip()]
|
|
|
|
if override_symbols:
|
|
symbols = override_symbols.copy()
|
|
else:
|
|
symbols = [sym.upper() for sym in dict.fromkeys(universe)]
|
|
pending_override = [str(sym).upper() for sym in state.get("orchestrator_pending_tickers", []) if sym]
|
|
incumbent = state.get("company_of_interest")
|
|
if incumbent and incumbent.upper() not in symbols:
|
|
symbols.append(incumbent.upper())
|
|
|
|
# Step 1: Gather live portfolio context for the universe
|
|
snapshots = context_fetcher(symbols)
|
|
quick_signals: Dict[str, Dict[str, str]] = {
|
|
str(key).upper(): value for key, value in (state.get("orchestrator_quick_signals", {}) or {}).items()
|
|
}
|
|
market_data_cache: Dict[str, str] = {
|
|
str(key).upper(): value for key, value in (state.get("orchestrator_market_data", {}) or {}).items()
|
|
}
|
|
|
|
first_snapshot = snapshots[0] if snapshots else {}
|
|
# Step 3: Ask the LLM to synthesize hypotheses and routing plan
|
|
system_prompt = (
|
|
"You are the head of trading. Review portfolio context, mandate, and quick signals to propose trading "
|
|
"hypotheses. Each hypothesis must include: ticker, rationale, priority (0-1), required_analysts list (subset "
|
|
"of ['market','social','news','fundamentals']), and immediate actions (monitor, abandon, escalate). Limit to the "
|
|
f"strongest {max_hypotheses} hypotheses above priority {threshold}. Respond with valid JSON containing keys "
|
|
"'hypotheses' (list), 'summary' (string), and 'status' (string)."
|
|
)
|
|
|
|
account_summary = _parse_account(first_snapshot.get("account", "")) if first_snapshot else {}
|
|
positions_summary = _parse_positions(first_snapshot.get("positions", "")) if first_snapshot else []
|
|
buying_power_value = _to_float(account_summary.get("buying_power") or account_summary.get("buying_power_usd"))
|
|
cash_value = _to_float(account_summary.get("cash") or account_summary.get("cash_usd"))
|
|
portfolio_value = _to_float(
|
|
account_summary.get("portfolio_value")
|
|
or account_summary.get("equity")
|
|
or account_summary.get("equity_value")
|
|
)
|
|
current_holdings = set()
|
|
for item in positions_summary:
|
|
symbol = (item.get("symbol") or item.get("symbol:") or "").upper()
|
|
qty_raw = item.get("quantity", "")
|
|
qty_value = 0.0
|
|
if qty_raw:
|
|
match = re.search(r"[-+]?\d*\.?\d+", qty_raw.replace(",", ""))
|
|
if match:
|
|
try:
|
|
qty_value = float(match.group(0))
|
|
except ValueError:
|
|
qty_value = 0.0
|
|
if symbol and qty_value != 0.0:
|
|
current_holdings.add(symbol)
|
|
universe_gap = [sym for sym in symbols if sym not in current_holdings]
|
|
|
|
payload = {
|
|
"profile": {
|
|
"name": profile_name,
|
|
"mandate": mandate,
|
|
"risk_limits": risk_limits,
|
|
"notes": notes,
|
|
"risk_summary": risk_blurb,
|
|
},
|
|
"portfolio_snapshots": snapshots,
|
|
"quick_signals": {},
|
|
"current_hypotheses": state.get("orchestrator_hypotheses", []),
|
|
"account_summary": account_summary,
|
|
"positions": positions_summary,
|
|
"existing_holdings": list(current_holdings),
|
|
"universe_candidates": universe_gap,
|
|
"trade_policy": trade_policy,
|
|
}
|
|
|
|
response = llm.invoke(
|
|
[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": json.dumps(payload)},
|
|
]
|
|
)
|
|
|
|
try:
|
|
parsed = json.loads(response.content if hasattr(response, "content") else str(response))
|
|
except json.JSONDecodeError:
|
|
parsed = {"hypotheses": [], "notes": "Failed to parse orchestrator response."}
|
|
|
|
hypotheses_raw = parsed.get("hypotheses", [])[:max_hypotheses]
|
|
if override_symbols:
|
|
allowed = set(override_symbols)
|
|
filtered = [item for item in hypotheses_raw if isinstance(item, dict) and str(item.get("ticker", "")).upper() in allowed]
|
|
if filtered:
|
|
hypotheses = filtered
|
|
else:
|
|
hypotheses = [hypotheses_raw[0]] if hypotheses_raw else []
|
|
else:
|
|
hypotheses = hypotheses_raw
|
|
summary_text = parsed.get("summary") or first_snapshot.get("summary_prompt", "")
|
|
status_text = parsed.get("status") or first_snapshot.get("status", "ok")
|
|
|
|
trade_priority_threshold = float(trade_policy.get("priority_threshold", 0.8))
|
|
min_cash_abs = float(trade_policy.get("min_cash_absolute", 0))
|
|
min_cash_ratio = float(trade_policy.get("min_cash_ratio", 0))
|
|
min_cash_required = max(min_cash_abs, portfolio_value * min_cash_ratio)
|
|
|
|
if not hypotheses:
|
|
fallback_ticker = None
|
|
if override_symbols:
|
|
fallback_ticker = override_symbols[0]
|
|
elif symbols:
|
|
fallback_ticker = symbols[0]
|
|
elif incumbent:
|
|
fallback_ticker = incumbent.upper()
|
|
elif state.get("company_of_interest"):
|
|
fallback_ticker = str(state.get("company_of_interest")).upper()
|
|
|
|
if fallback_ticker:
|
|
fallback_required = ["market", "news", "fundamentals"]
|
|
can_trade = buying_power_value >= min_cash_required
|
|
fallback_action = "trade" if can_trade else "escalate"
|
|
fallback_priority = max(trade_priority_threshold, 0.8)
|
|
fallback = {
|
|
"ticker": fallback_ticker,
|
|
"rationale": (
|
|
f"Generated fallback hypothesis for {fallback_ticker} to evaluate trading opportunities with available buying power "
|
|
f"of ${buying_power_value:,.0f}. Analysts should validate signals before execution."
|
|
),
|
|
"priority": fallback_priority,
|
|
"required_analysts": fallback_required,
|
|
"immediate_actions": fallback_action,
|
|
}
|
|
hypotheses = [fallback]
|
|
if override_symbols and fallback_ticker not in override_symbols:
|
|
override_symbols.append(fallback_ticker)
|
|
if fallback_ticker not in symbols:
|
|
symbols.insert(0, fallback_ticker)
|
|
|
|
candidate_pool = override_symbols or symbols
|
|
|
|
try:
|
|
print(f"[Orchestrator] Override focus: {override_symbols if override_symbols else '<none>'}")
|
|
except Exception:
|
|
pass
|
|
|
|
focus_symbols: List[str] = []
|
|
hypothesis_map: Dict[str, Dict[str, Any]] = {}
|
|
for item in hypotheses:
|
|
ticker_value = str(item.get("ticker", "")).upper() if isinstance(item, dict) else ""
|
|
if ticker_value and ticker_value not in focus_symbols:
|
|
focus_symbols.append(ticker_value)
|
|
if isinstance(item, dict):
|
|
hypothesis_map[ticker_value] = item
|
|
if ticker_value and isinstance(item, dict):
|
|
hypothesis_map.setdefault(ticker_value, item)
|
|
for holding in current_holdings:
|
|
if holding not in focus_symbols:
|
|
focus_symbols.append(holding)
|
|
if incumbent:
|
|
incumbent_u = incumbent.upper()
|
|
if incumbent_u and incumbent_u not in focus_symbols:
|
|
focus_symbols.append(incumbent_u)
|
|
if not focus_symbols:
|
|
focus_symbols = symbols[: max_hypotheses or 1]
|
|
else:
|
|
for symbol in symbols:
|
|
if symbol not in focus_symbols:
|
|
focus_symbols.append(symbol)
|
|
|
|
if pending_override:
|
|
ordered_focus: List[str] = []
|
|
for sym in pending_override:
|
|
if sym in focus_symbols and sym not in ordered_focus:
|
|
ordered_focus.append(sym)
|
|
for sym in focus_symbols:
|
|
if sym not in ordered_focus:
|
|
ordered_focus.append(sym)
|
|
focus_symbols = ordered_focus
|
|
|
|
try:
|
|
print(f"[Orchestrator] Focus tickers: {', '.join(focus_symbols)}")
|
|
except Exception: # pragma: no cover - defensive logging
|
|
pass
|
|
|
|
if pending_override:
|
|
active_ticker = pending_override[0]
|
|
elif focus_symbols:
|
|
active_ticker = focus_symbols[0]
|
|
else:
|
|
active_ticker = state.get("target_ticker", state.get("company_of_interest", "")).upper()
|
|
|
|
active = hypothesis_map.get(active_ticker)
|
|
if active is None and hypotheses:
|
|
fallback = hypotheses[0]
|
|
if isinstance(fallback, dict) and fallback.get("ticker"):
|
|
active_ticker = str(fallback.get("ticker")).upper()
|
|
active = fallback
|
|
|
|
scheduled_sequence: List[str] = []
|
|
if active and isinstance(active, dict):
|
|
for analyst in active.get("required_analysts", []):
|
|
role = str(analyst).lower()
|
|
if role not in scheduled_sequence:
|
|
scheduled_sequence.append(role)
|
|
immediate_action = (active.get("immediate_actions") or active.get("action") or "").lower() if isinstance(active, dict) else ""
|
|
|
|
priority_val = float(active.get("priority") or 0) if isinstance(active, dict) else 0.0
|
|
|
|
holding_active = active_ticker in current_holdings
|
|
if isinstance(active, dict) and immediate_action in {"", "monitor"}:
|
|
if priority_val >= trade_priority_threshold:
|
|
if not holding_active and buying_power_value >= min_cash_required:
|
|
immediate_action = "trade"
|
|
active["immediate_actions"] = "trade"
|
|
else:
|
|
immediate_action = "escalate"
|
|
active["immediate_actions"] = "escalate"
|
|
|
|
default_analysis_order = ["market", "news", "social", "fundamentals"]
|
|
analysis_candidates = [
|
|
item for item in scheduled_sequence if item in {"market", "social", "news", "fundamentals"}
|
|
]
|
|
try:
|
|
if analysis_candidates:
|
|
print(f"[Orchestrator] Hypothesis requests analysts {analysis_candidates} for {active_ticker}.")
|
|
else:
|
|
print(f"[Orchestrator] No explicit analyst order from hypothesis; defaulting to {default_analysis_order}.")
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
print(
|
|
f"[Orchestrator] Active {active_ticker or '<none>'} priority {priority_val:.2f} -> {immediate_action.upper()} "
|
|
f"(cash ${cash_value:,.0f}, buying power ${buying_power_value:,.0f}, min cash ${min_cash_required:,.0f})"
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
planner_raw: Dict[str, Any] = {}
|
|
planner_actions: List[str] = []
|
|
planner_immediate: str = ""
|
|
planner_notes: str = ""
|
|
planner_next_directive: Optional[str] = None
|
|
ticker_plan_summaries: Dict[str, Any] = dict(state.get("orchestrator_ticker_plans", {}) or {})
|
|
|
|
ACTION_ALIASES = {
|
|
"market": "market",
|
|
"market_analyst": "market",
|
|
"run_market": "market",
|
|
"run_market_analyst": "market",
|
|
"news": "news",
|
|
"news_analyst": "news",
|
|
"run_news": "news",
|
|
"social": "social",
|
|
"social_analyst": "social",
|
|
"fundamentals": "fundamentals",
|
|
"fundamental": "fundamentals",
|
|
"fundamentals_analyst": "fundamentals",
|
|
"debate": "debate",
|
|
"research_debate": "debate",
|
|
"manager": "manager",
|
|
"research_manager": "manager",
|
|
"trader": "trader",
|
|
"risk": "risk",
|
|
"risk_manager": "risk",
|
|
"orchestrator": "orchestrator",
|
|
"stop": "end",
|
|
"end": "end",
|
|
}
|
|
|
|
def normalise_action(value: str) -> Optional[str]:
|
|
key = (value or "").strip().lower()
|
|
return ACTION_ALIASES.get(key)
|
|
|
|
def append_unique(target: List[str], items: List[str]) -> None:
|
|
seen = set(target)
|
|
for elem in items:
|
|
if elem not in seen:
|
|
target.append(elem)
|
|
seen.add(elem)
|
|
|
|
trade_date = state.get("trade_date", "")
|
|
|
|
def _coerce_trade_date(value: str) -> date:
|
|
if not value:
|
|
return date.today()
|
|
try:
|
|
dt_value = datetime.fromisoformat(value)
|
|
except ValueError:
|
|
try:
|
|
dt_value = datetime.fromisoformat(f"{value}T00:00:00")
|
|
except ValueError:
|
|
dt_value = datetime.today()
|
|
return dt_value.date()
|
|
|
|
trade_date_obj = _coerce_trade_date(trade_date)
|
|
start_dt = trade_date_obj - timedelta(days=max(1, int(market_lookback)))
|
|
start_date_str = start_dt.isoformat()
|
|
end_date_str = trade_date_obj.isoformat()
|
|
|
|
market_data: Dict[str, str] = dict(market_data_cache)
|
|
|
|
for symbol in focus_symbols:
|
|
cache_key = symbol.upper()
|
|
if cache_key not in quick_signals or not quick_signals.get(cache_key):
|
|
try:
|
|
quick_signals[cache_key] = fast_news_fetcher(symbol, trade_date, sentiment_lookback, headline_limit)
|
|
except Exception as exc: # pragma: no cover - vendor failure is non-critical
|
|
quick_signals[cache_key] = {"error": str(exc)}
|
|
if cache_key not in market_data or not market_data.get(cache_key):
|
|
try:
|
|
market_data[cache_key] = route_to_vendor("get_stock_data", symbol, start_date_str, end_date_str)
|
|
except Exception as exc: # pragma: no cover - vendor failure is non-critical
|
|
market_data[cache_key] = f"Error fetching market data: {exc}"
|
|
|
|
try:
|
|
print(
|
|
"[Orchestrator] Retrieved market data for: "
|
|
+ ", ".join(sorted(market_data.keys()))
|
|
)
|
|
except Exception: # pragma: no cover - defensive logging
|
|
pass
|
|
|
|
existing_plan = ticker_plan_summaries.get(active_ticker or "", {}) if active_ticker else {}
|
|
if plan_generator is not None:
|
|
planner_payload = {
|
|
"profile": payload["profile"],
|
|
"hypotheses": hypotheses,
|
|
"active_hypothesis": active,
|
|
"summary": summary_text,
|
|
"status": status_text,
|
|
"account_summary": account_summary,
|
|
"positions_summary": positions_summary,
|
|
"portfolio_snapshots": snapshots,
|
|
"quick_signals": quick_signals,
|
|
"market_data": market_data,
|
|
"focus_symbols": focus_symbols,
|
|
"focus_symbol": active_ticker,
|
|
"trade_policy": trade_policy,
|
|
"buying_power": buying_power_value,
|
|
"cash_available": cash_value,
|
|
"portfolio_value": portfolio_value,
|
|
}
|
|
if existing_plan:
|
|
planner_raw = existing_plan
|
|
else:
|
|
planner_result = plan_generator(planner_payload) or {}
|
|
if isinstance(planner_result, dict):
|
|
planner_raw = planner_result
|
|
else:
|
|
planner_raw = {"text": str(planner_result)}
|
|
ticker_plan_summaries[active_ticker or "<unknown>"] = planner_raw if planner_raw else {}
|
|
|
|
if isinstance(planner_raw, dict):
|
|
plan_structured = planner_raw.get("structured")
|
|
planner_notes = planner_raw.get("text") or planner_raw.get("notes", "")
|
|
if plan_structured is None:
|
|
plan_text = planner_raw.get("text")
|
|
if plan_text:
|
|
try:
|
|
plan_structured = json.loads(plan_text)
|
|
except json.JSONDecodeError:
|
|
plan_structured = None
|
|
if plan_structured is None:
|
|
plan_structured = planner_raw.get("plan")
|
|
else:
|
|
plan_structured = None
|
|
|
|
if isinstance(plan_structured, str):
|
|
try:
|
|
plan_structured = json.loads(plan_structured)
|
|
except json.JSONDecodeError:
|
|
plan_structured = None
|
|
|
|
if isinstance(plan_structured, dict):
|
|
raw_actions = plan_structured.get("actions") or plan_structured.get("steps") or []
|
|
planner_immediate = str(plan_structured.get("next_decision") or "").lower()
|
|
planner_notes = planner_notes or plan_structured.get("notes", "")
|
|
planner_next_directive = plan_structured.get("next_directive")
|
|
elif isinstance(plan_structured, list):
|
|
raw_actions = plan_structured
|
|
else:
|
|
raw_actions = []
|
|
|
|
if planner_immediate and not immediate_action:
|
|
immediate_action = planner_immediate
|
|
|
|
for item in raw_actions:
|
|
if isinstance(item, str):
|
|
normalized = normalise_action(item)
|
|
elif isinstance(item, dict):
|
|
action_value = item.get("action") or item.get("name") or item.get("tool")
|
|
normalized = normalise_action(str(action_value)) if action_value else None
|
|
if not planner_notes and item.get("reason"):
|
|
planner_notes = str(item.get("reason"))
|
|
else:
|
|
normalized = None
|
|
if normalized:
|
|
planner_actions.append(normalized)
|
|
|
|
for symbol in focus_symbols:
|
|
if symbol == active_ticker:
|
|
continue
|
|
if override_symbols and symbol not in override_symbols:
|
|
continue
|
|
extra_payload = {
|
|
"profile": payload["profile"],
|
|
"hypotheses": hypotheses,
|
|
"active_hypothesis": hypothesis_map.get(symbol),
|
|
"summary": summary_text,
|
|
"status": status_text,
|
|
"account_summary": account_summary,
|
|
"positions_summary": positions_summary,
|
|
"portfolio_snapshots": snapshots,
|
|
"quick_signals": quick_signals,
|
|
"market_data": market_data,
|
|
"focus_symbols": focus_symbols,
|
|
"focus_symbol": symbol,
|
|
"trade_policy": trade_policy,
|
|
"buying_power": buying_power_value,
|
|
"cash_available": cash_value,
|
|
"portfolio_value": portfolio_value,
|
|
}
|
|
if symbol in ticker_plan_summaries and ticker_plan_summaries[symbol]:
|
|
continue
|
|
try:
|
|
extra_result = plan_generator(extra_payload) or {}
|
|
except Exception as exc: # pragma: no cover - planner failures shouldn't halt orchestration
|
|
extra_result = {"error": str(exc)}
|
|
if isinstance(extra_result, dict):
|
|
ticker_plan_summaries[symbol] = extra_result
|
|
else:
|
|
ticker_plan_summaries[symbol] = {"text": str(extra_result)}
|
|
|
|
action_queue: List[str] = []
|
|
if planner_actions:
|
|
append_unique(action_queue, planner_actions)
|
|
try:
|
|
print(f"[Orchestrator] Sequential planner actions for {active_ticker}: {planner_actions}")
|
|
except Exception:
|
|
pass
|
|
else:
|
|
append_unique(action_queue, analysis_candidates or default_analysis_order)
|
|
|
|
if immediate_action in {"escalate", "trade", "execute"}:
|
|
append_unique(action_queue, ["debate"])
|
|
append_unique(action_queue, ["manager"])
|
|
append_unique(action_queue, ["trader"])
|
|
if immediate_action in {"trade", "execute"}:
|
|
append_unique(action_queue, ["risk"])
|
|
|
|
next_directive_value = planner_next_directive or "end"
|
|
if override_symbols:
|
|
remaining_focus = []
|
|
next_directive_value = "end"
|
|
else:
|
|
remaining_focus = [sym for sym in focus_symbols if sym != active_ticker]
|
|
if remaining_focus and next_directive_value == "end":
|
|
next_directive_value = "orchestrator"
|
|
|
|
try:
|
|
print(
|
|
"[Orchestrator] Queue:"
|
|
f" focus={focus_symbols} | active={active_ticker} | immediate={immediate_action} | queue={action_queue}"
|
|
)
|
|
print(
|
|
"[Orchestrator] Ticker plans generated for: "
|
|
+ ", ".join(sorted(ticker_plan_summaries.keys()))
|
|
)
|
|
except Exception: # pragma: no cover - defensive logging
|
|
pass
|
|
|
|
# Log initial hypothesis for debugging
|
|
try:
|
|
formatted = json.dumps(
|
|
{
|
|
"hypotheses": hypotheses,
|
|
"summary": summary_text,
|
|
"status": status_text,
|
|
"action": immediate_action,
|
|
"queue": action_queue,
|
|
"planner_plan": planner_raw,
|
|
},
|
|
indent=2,
|
|
)
|
|
print(f"[Orchestrator] Initial hypotheses:\n{formatted}")
|
|
except Exception:
|
|
pass
|
|
|
|
analyst_schedule = [item for item in action_queue if item in {"market", "social", "news", "fundamentals"}]
|
|
if planner_actions:
|
|
plan_snapshot = planner_actions
|
|
else:
|
|
plan_snapshot = analysis_candidates or [item for item in default_analysis_order if item in action_queue]
|
|
|
|
serializable_plan = planner_raw
|
|
if isinstance(planner_raw, dict):
|
|
serializable_plan = {k: v for k, v in planner_raw.items() if k != "raw"}
|
|
if "raw" in planner_raw:
|
|
try:
|
|
serializable_plan.setdefault("raw_repr", repr(planner_raw["raw"]))
|
|
except Exception:
|
|
serializable_plan.setdefault("raw_repr", "<non-serializable>")
|
|
|
|
state_update = {
|
|
"messages": [response],
|
|
"portfolio_profile": profile,
|
|
"portfolio_summary": summary_text,
|
|
"orchestrator_status": status_text,
|
|
"alpaca_account_text": first_snapshot.get("account", ""),
|
|
"alpaca_positions_text": first_snapshot.get("positions", ""),
|
|
"alpaca_orders_text": first_snapshot.get("orders", ""),
|
|
"orchestrator_hypotheses": hypotheses,
|
|
"active_hypothesis": active,
|
|
"scheduled_analysts": analyst_schedule,
|
|
"scheduled_analysts_plan": plan_snapshot,
|
|
"company_of_interest": active_ticker or state.get("company_of_interest"),
|
|
"target_ticker": active_ticker,
|
|
"orchestrator_action": immediate_action,
|
|
"portfolio_account_summary": account_summary,
|
|
"portfolio_positions_summary": positions_summary,
|
|
"orchestrator_focus_symbols": focus_symbols,
|
|
"orchestrator_quick_signals": quick_signals,
|
|
"orchestrator_market_data": market_data,
|
|
"orchestrator_ticker_plans": ticker_plan_summaries,
|
|
"orchestrator_pending_tickers": remaining_focus,
|
|
"orchestrator_buying_power": buying_power_value,
|
|
"orchestrator_cash_available": cash_value,
|
|
"orchestrator_portfolio_value": portfolio_value,
|
|
"action_queue": action_queue,
|
|
"next_directive": next_directive_value,
|
|
"planner_plan": serializable_plan,
|
|
"planner_notes": planner_notes,
|
|
"orchestrator_focus_override": override_symbols,
|
|
}
|
|
|
|
return state_update
|
|
|
|
return orchestrator_node
|