122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
"""Holding Reviewer LLM agent.
|
|
|
|
Reviews all open positions in a portfolio and recommends HOLD or SELL for each,
|
|
based on current P&L, price momentum, and news sentiment.
|
|
|
|
Pattern: ``create_holding_reviewer(llm)`` → closure (scanner agent pattern).
|
|
Uses ``run_tool_loop()`` for inline tool execution.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
|
|
|
from tradingagents.agents.utils.core_stock_tools import get_stock_data
|
|
from tradingagents.agents.utils.json_utils import extract_json
|
|
from tradingagents.agents.utils.news_data_tools import get_news
|
|
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def create_holding_reviewer(llm):
|
|
"""Create a holding reviewer agent node.
|
|
|
|
Args:
|
|
llm: A LangChain chat model instance.
|
|
|
|
Returns:
|
|
A node function ``holding_reviewer_node(state)`` compatible with LangGraph.
|
|
"""
|
|
|
|
def holding_reviewer_node(state):
|
|
portfolio_data_str = state.get("portfolio_data") or "{}"
|
|
analysis_date = state.get("analysis_date") or ""
|
|
|
|
try:
|
|
portfolio_data = json.loads(portfolio_data_str)
|
|
except (json.JSONDecodeError, TypeError):
|
|
portfolio_data = {}
|
|
|
|
holdings = portfolio_data.get("holdings") or []
|
|
portfolio_name = portfolio_data.get("portfolio", {}).get("name", "Portfolio")
|
|
|
|
if not holdings:
|
|
return {
|
|
"holding_reviews": json.dumps({}),
|
|
"sender": "holding_reviewer",
|
|
}
|
|
|
|
holdings_summary = "\n".join(
|
|
f"- {h.get('ticker', '?')}: {h.get('shares', 0):.2f} shares @ avg cost "
|
|
f"${h.get('avg_cost', 0):.2f} | sector: {h.get('sector', 'Unknown')}"
|
|
for h in holdings
|
|
)
|
|
|
|
tools = [get_stock_data, get_news]
|
|
|
|
system_message = (
|
|
f"You are a portfolio analyst reviewing all open positions in '{portfolio_name}'. "
|
|
f"The analysis date is {analysis_date}. "
|
|
f"You hold the following positions:\n{holdings_summary}\n\n"
|
|
"For each holding, use get_stock_data to retrieve recent price history "
|
|
"and get_news to check recent sentiment. "
|
|
"Then produce a JSON object where each key is a ticker symbol and the value is:\n"
|
|
"{\n"
|
|
' "ticker": "...",\n'
|
|
' "recommendation": "HOLD" or "SELL",\n'
|
|
' "confidence": "high" or "medium" or "low",\n'
|
|
' "rationale": "...",\n'
|
|
' "key_risks": ["..."]\n'
|
|
"}\n\n"
|
|
"Consider: current unrealized P&L, price momentum, news sentiment, "
|
|
"and whether the original thesis still holds. "
|
|
"Output ONLY valid JSON with ticker → review mapping. "
|
|
"Start your final response with '{' and end with '}'. "
|
|
"Do NOT use markdown code fences."
|
|
)
|
|
|
|
prompt = ChatPromptTemplate.from_messages(
|
|
[
|
|
(
|
|
"system",
|
|
"You are a helpful AI assistant, collaborating with other assistants."
|
|
" Use the provided tools to progress towards answering the question."
|
|
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
|
" will help where you left off. Execute what you can to make progress."
|
|
" You have access to the following tools: {tool_names}.\n{system_message}"
|
|
" For your reference, the current date is {current_date}.",
|
|
),
|
|
MessagesPlaceholder(variable_name="messages"),
|
|
]
|
|
)
|
|
|
|
prompt = prompt.partial(system_message=system_message)
|
|
prompt = prompt.partial(tool_names=", ".join([t.name for t in tools]))
|
|
prompt = prompt.partial(current_date=analysis_date)
|
|
|
|
chain = prompt | llm.bind_tools(tools)
|
|
result = run_tool_loop(chain, state["messages"], tools)
|
|
|
|
raw = result.content or "{}"
|
|
try:
|
|
parsed = extract_json(raw)
|
|
reviews_str = json.dumps(parsed)
|
|
except (ValueError, json.JSONDecodeError):
|
|
logger.warning(
|
|
"holding_reviewer: could not extract JSON; storing raw (first 200): %s",
|
|
raw[:200],
|
|
)
|
|
reviews_str = raw
|
|
|
|
return {
|
|
"messages": [result],
|
|
"holding_reviews": reviews_str,
|
|
"sender": "holding_reviewer",
|
|
}
|
|
|
|
return holding_reviewer_node
|