Skip to content

Commit b289263

Browse files
committed
fix(tradingagents): 修复深度分析数据串台与决策展示不匹配
数据串台(最严重):并发跑多只股票深度分析时,_PANWATCH_DATA_CACHE 模块级全局 dict 被后启动的任务覆盖,导致广汽 601238 的报告混入赛力斯 601127 的 K线/价格/新闻。改为 ContextVar(asyncio.to_thread 会 copy_context,每个并发 worker 拿独立副本,天然隔离);route_to_vendor 的 monkeypatch 改引用计数 + 锁,消除安装/卸载嵌套竞态。 决策与正文不匹配:正文写"卖出/Sell"但顶部显示"持有"。评级提取原先 优先信任上游 propagate() 二次提炼的 decision(会失真),改为优先解析 PM 正文(用户可见的最终决策书)显式标签,优先级 正文标签>decision> 模糊兜底;新增 _parse_rating_label 只做精确标签匹配避免干扰词误判。 新增 tests/test_tradingagents_toolkit_isolation.py 并发隔离测试, test_tradingagents_5_tier_rating.py 增正文/decision 冲突回归。
1 parent bad5157 commit b289263

4 files changed

Lines changed: 357 additions & 162 deletions

File tree

src/agents/tradingagents/result_mapper.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ def map_state_to_result(
5555
state = ta_result.get("final_state") or {}
5656
cost_usd = float(ta_result.get("cost_usd", 0.0) or 0.0)
5757

58-
# 上游 PM 返回 5 档评级("Buy"/"Overweight"/"Hold"/"Underweight"/"Sell")
59-
# 如果 propagate() 第二个返回拿不到 5 档值,从 final_trade_decision 文本里 fallback 解析
60-
rating_raw = (ta_result.get("decision") or "").strip().lower()
58+
# 评级以 PM 正文(final_trade_decision,用户实际看到的最终决策书)为权威来源:
59+
# 上游 propagate() 第二个返回的 decision 是对正文的二次提炼,会失真(正文写"卖出"
60+
# 却返回 "HOLD"),所以优先解析正文里的显式评级标签。
61+
# 优先级:正文显式标签 > 上游 decision > 正文模糊扫描兜底。
62+
final_text = state.get("final_trade_decision") or ""
63+
rating_raw = _parse_rating_label(final_text)
6164
if rating_raw not in RATING_LABEL_MAP:
62-
rating_raw = _parse_rating_from_text(state.get("final_trade_decision") or "")
65+
rating_raw = (ta_result.get("decision") or "").strip().lower()
66+
if rating_raw not in RATING_LABEL_MAP:
67+
rating_raw = _parse_rating_from_text(final_text)
6368

6469
action = RATING_ACTION_MAP.get(rating_raw, "hold")
6570
action_label = RATING_LABEL_MAP.get(rating_raw, "持有")
@@ -120,8 +125,12 @@ def map_state_to_result(
120125
}
121126

122127

123-
def _parse_rating_from_text(text: str) -> str:
124-
"""从文本里抽 5 档评级。优先 'Rating: X' 标签,然后第一个 5 档词。"""
128+
def _parse_rating_label(text: str) -> str:
129+
"""只解析 PM 正文里的**显式评级标签**(最终交易决策/评级/FINAL TRANSACTION PROPOSAL: X)。
130+
131+
不做模糊关键词扫描 —— 避免正文里"否决了之前的买入建议"这类干扰词被误判。
132+
用作评级提取的首选,确保展示与用户可见的最终决策书一致。
133+
"""
125134
if not text:
126135
return ""
127136
m = _RATING_TEXT_RE.search(text)
@@ -131,6 +140,16 @@ def _parse_rating_from_text(text: str) -> str:
131140
word = _RATING_ZH_TO_EN[word]
132141
if word in RATING_LABEL_MAP:
133142
return word
143+
return ""
144+
145+
146+
def _parse_rating_from_text(text: str) -> str:
147+
"""从文本里抽 5 档评级。优先 'Rating: X' 标签,然后第一个 5 档词。"""
148+
if not text:
149+
return ""
150+
label = _parse_rating_label(text)
151+
if label:
152+
return label
134153
# 兜底:扫描整段文本里第一个出现的 5 档英文/中文词
135154
text_low = text.lower()
136155
for word in ("overweight", "underweight", "buy", "sell", "hold"):

0 commit comments

Comments
 (0)